Cache de arquivos estáticos com Nginx

Servir arquivos estáticos diretamente pelo Nginx, sem passar pelo Node.js ou Python, é 10-100x mais rápido. Com cache de proxy configurado, o Nginx armazena respostas do backend e as serve para requisições subsequentes sem chegar na aplicação. Isso reduz latência, carga no backend e consumo de memória.

Servir arquivos estáticos diretamente

Para arquivos que ficam no disco da VPS, sirva direto pelo Nginx:

nginx
server {
    listen 443 ssl http2;
    server_name seudominio.com.br;

    # Diretório raiz dos estáticos
    root /var/www/html;

    # Servir arquivos estáticos sem passar pelo backend
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
        expires 1y;                        # cache por 1 ano no browser
        add_header Cache-Control "public, immutable";
        add_header Vary "Accept-Encoding";
        access_log off;                    # não logar para economizar IO
        try_files $uri =404;
    }

    # HTML: sem cache (sempre servir versão atualizada)
    location ~* \.html$ {
        expires -1;
        add_header Cache-Control "no-store, no-cache, must-revalidate";
    }

    # API: sem cache
    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Tentar arquivo estático, fallback para a app
    location / {
        try_files $uri $uri/ @app;
    }

    location @app {
        proxy_pass http://127.0.0.1:3000;
    }
}

Proxy cache: armazenar respostas do backend

O Nginx pode cachear respostas HTTP do backend em disco:

nginx
# /etc/nginx/nginx.conf — dentro do bloco http {}
proxy_cache_path /var/cache/nginx
    levels=1:2
    keys_zone=api_cache:10m
    max_size=1g
    inactive=60m
    use_temp_path=off;

# No bloco server:
server {
    location /api/produtos {
        # Cachear respostas GET por 5 minutos
        proxy_cache api_cache;
        proxy_cache_valid 200 5m;
        proxy_cache_valid 404 1m;
        proxy_cache_methods GET HEAD;
        proxy_cache_key "$scheme$request_method$host$request_uri";

        # Usar cache mesmo quando backend estiver offline
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
        proxy_cache_background_update on;
        proxy_cache_lock on;       # apenas 1 req passa ao backend por key (thundering herd)

        # Adicionar header mostrando se veio do cache
        add_header X-Cache-Status $upstream_cache_status;

        proxy_pass http://127.0.0.1:3000;
    }
}

Compressão gzip e brotli

Comprimir respostas reduz o tamanho em 60-80% para texto/JSON:

nginx
# /etc/nginx/conf.d/compression.conf

# Gzip (suporte universal)
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;       # nível 1-9: 6 é bom balanço velocidade/compressão
gzip_min_length 1024;    # não comprimir arquivos < 1 KB
gzip_types
    text/plain
    text/css
    text/javascript
    application/javascript
    application/json
    application/xml
    image/svg+xml
    font/woff
    font/woff2;

# Brotli (melhor compressão que gzip, suporte browsers modernos)
# Requer módulo ngx_brotli (não incluído por padrão):
# sudo apt install libbrotli-dev
# Compilar Nginx com --add-module=ngx_brotli
# Ou usar nginx-extra: sudo apt install nginx-extras
brotli on;
brotli_comp_level 6;
brotli_types
    text/plain
    text/css
    application/javascript
    application/json
    image/svg+xml;

# Servir arquivos pré-comprimidos (.gz ou .br) se existirem:
gzip_static on;
# brotli_static on;

Invalidar o proxy cache

Limpar o cache quando o conteúdo do backend é atualizado:

nginx
# Limpar cache por chave específica:
sudo find /var/cache/nginx -type f -name "*.cache" | head -10

# Limpar todo o cache de uma zona:
sudo rm -rf /var/cache/nginx/*
sudo nginx -s reload

# Purge seletivo com ngx_cache_purge (módulo third-party):
# Adicionar ao location:
location ~ /purge(/.*) {
    allow 127.0.0.1;
    deny all;
    proxy_cache_purge api_cache $scheme$request_method$host$1;
}

# Chamar a partir do deploy script:
curl -X PURGE http://127.0.0.1/purge/api/produtos

# Forçar bypass do cache (sem purge) — útil para testes:
location /api/ {
    # Bypass se o header X-No-Cache estiver presente:
    proxy_cache_bypass $http_x_no_cache;
    proxy_no_cache $http_x_no_cache;
    proxy_cache api_cache;
    proxy_pass http://127.0.0.1:3000;
}
# Testar: curl -H "X-No-Cache: 1" https://api.seudominio.com.br/api/produtos

Verificar eficiência do cache

Monitorar o hit rate e identificar o que está sendo cacheado:

bash
# Verificar o header X-Cache-Status na resposta:
curl -I https://api.seudominio.com.br/api/produtos
# X-Cache-Status: MISS    → primeira requisição, foi ao backend
# X-Cache-Status: HIT     → servido do cache
# X-Cache-Status: EXPIRED → cache expirado, foi ao backend
# X-Cache-Status: STALE   → cache expirado mas backend offline
# X-Cache-Status: BYPASS  → forçado a ir ao backend

# Calcular hit rate nos logs:
sudo awk '{print $NF}' /var/log/nginx/access.log \
  | grep -oP 'X-Cache-Status: \K\w+' \
  | sort | uniq -c

# Alternativa — logar o cache status:
log_format cache_log '$remote_addr - [$time_local] "$request" $status '
                     '$upstream_cache_status "$http_referer"';
access_log /var/log/nginx/cache.log cache_log;

# Tamanho atual do cache em disco:
sudo du -sh /var/cache/nginx/

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Cache no Nginx: para respostas HTTP completas (headers + body), sem código na aplicação, transparente. Cache no Redis: para dados estruturados que múltiplos endpoints usam, com invalidação granular, compartilhado entre múltiplas instâncias da aplicação. Use os dois: Nginx para respostas públicas sem autenticação, Redis para dados de negócio que precisam de invalidação precisa.

Configure proxy_cache_bypass e proxy_no_cache para requisições com cookie de sessão ou header Authorization: proxy_cache_bypass $http_authorization $cookie_session; proxy_no_cache $http_authorization $cookie_session;. Assim, requisições autenticadas sempre vão ao backend e não são armazenadas no cache.

Sim, compressão usa CPU. Para arquivos pequenos (<1 KB): overhead de CPU supera o ganho de rede, por isso gzip_min_length 1024. Para a maioria das VPS: o custo de CPU do gzip é muito menor que o benefício de reduzir IO de rede e latência. Use gzip_comp_level 6 (bom balanço). Gzip nível 9 usa 3x mais CPU que nível 6 com ganho marginal de compressão.

Quando o cache expira, múltiplas requisições simultâneas chegam ao mesmo tempo, todas vão ao backend juntas. proxy_cache_lock on faz com que apenas a primeira requisição vá ao backend; as demais ficam aguardando e usam o resultado cacheado quando ele chegar. proxy_cache_use_stale updating serve o conteúdo antigo enquanto o novo está sendo buscado.

Use immutable apenas para arquivos cujo conteúdo nunca muda para aquela URL, tipicamente assets com hash no nome (main.a1b2c3.js, style.xyz789.css). Quando o código muda, o nome do arquivo muda (novo hash) e o browser baixa normalmente. Para arquivos sem hash no nome (favicon.ico, robots.txt): use max-age sem immutable.