Servicios

Configurar caché en Nginx como reverse proxy

Aprende a definir una zona de caché en Nginx, cachear assets estáticos y páginas HTML, y servir contenido incluso cuando el backend cae.

nginxcachereverse-proxyproxy_cachewebsysadmin

Si tienes Nginx delante de un backend, montarlo como reverse proxy con caché es de las mejores inversiones que puedes hacer. Te quitas de encima picos de tráfico, la web se siente más rápida aunque el origen sea un servidor modesto y, lo mejor, si el backend se cae o lo reinicias, los usuarios ni se enteran. La idea es simple: Nginx guarda en disco una copia de la respuesta y la sirve tal cual en las próximas peticiones, sin volver a molestar al backend hasta que esa copia caduque.

1. Definir la zona de caché

Lo primero es decirle a Nginx dónde guarda la caché y cuánta RAM reserva para el índice. Ojo, esta directiva va fuera del bloque server {}. Yo suelo definirla en el nginx.conf principal, dentro del bloque http {}, lo cual es útil si vas a usar la misma zona para varios virtualhosts que la necesiten.

proxy_cache_path /var/cache/nginx/docubits.altobits.com
    levels=1:2
    keys_zone=DOCUBITS_CACHE:10m
    max_size=1g
    inactive=60m
    use_temp_path=off;

Qué función cumple cada parámetro, para que no te lo tengas que mirar en el manual cada vez:

  • keys_zone=DOCUBITS_CACHE:10m — el nombre de la zona y 10 MB de RAM para el índice de claves. 10 MB da para cientos de miles de entradas.
  • max_size=1g — límite de espacio en disco. Cuando se llena, Nginx va borrando lo más antiguo.
  • inactive=60m — si una entrada no se pide en 60 minutos, se elimina. Útil para que la caché no se quede indefinidamente.
  • use_temp_path=off — escribe directo en la ruta final. Si lo dejas en on, Nginx primero escribe en /tmp y luego lo mueve, en la mayoría de casos es algo innecesario.

2. Bloque server con dos ubicaciones de caché

La optimización consiste en separar assets estáticos (imágenes, CSS, JS, fuentes) de las páginas HTML. No tiene sentido cachear un index.html una semana ni un .js con hash una hora.

server {
    listen 80;
    listen 443 ssl;
    server_name docubits.altobits.com;

    ssl_certificate     /etc/letsencrypt/live/docubits.altobits.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/docubits.altobits.com/privkey.pem;

    # 1. Caché agresiva para assets estáticos
    location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|woff2?|eot|ttf|otf)$ {
        proxy_pass http://127.0.0.1:8080;

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        proxy_cache DOCUBITS_CACHE;
        proxy_cache_valid 200 302 7d;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_lock on;

        expires 1y;
        add_header Cache-Control "public, no-transform";
        add_header X-Cache-Status $upstream_cache_status;
    }

    # 2. Páginas HTML
    location / {
        proxy_pass http://127.0.0.1:8080;

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering on;

        proxy_cache DOCUBITS_CACHE;
        proxy_ignore_headers Cache-Control Expires;
        proxy_cache_valid 200 1h;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_lock on;

        add_header X-Cache-Status $upstream_cache_status;
    }
}

3. Qué hace cada directiva (y por qué está)

proxy_cache DOCUBITS_CACHE

Enciende la caché apuntando a la zona del paso 1. Sin esta línea, las demás directivas de caché no hacen absolutamente nada.

proxy_cache_valid 200 1h (HTML) y proxy_cache_valid 200 302 7d (assets)

Cuánto tiempo se considera útil cada respuesta. Los assets se definen 7 días porque, en mi caso, sus nombres llevan hash y el contenido nunca cambia; las páginas HTML se renuevan cada hora, que es un equilibrio razonable entre carga y actualización del contenido. Si tus estáticos no están basados en un hash quizás deberías usar un tiempo de vida de la caché más conservador.

proxy_cache_use_stale

Si el backend falla (timeout, 500, 502, 503, 504) o se está actualizando, Nginx no le enseña un error al usuario: le entrega la última copia que tenga en caché. Es tu red de seguridad cuando estás desplegando o cuando el backend decide tomarse un descanso.

proxy_cache_lock on

Evita el cache stampede. Imagina que la entrada acaba de expirar y entran 100 peticiones a la vez: con esto activo, solo la primera pasa al backend a regenerarla; las otras 99 se quedan en cola unos milisegundos y reciben la respuesta en caché recién creada. Sin él, las 100 peticiones consultarán el backend a la vez.

proxy_ignore_headers Cache-Control Expires

Fuerza a Nginx a fiarse de tu proxy_cache_valid en lugar de las cabeceras que mande el backend. Esto me salvó la vida con un backend que mandaba no-cache por defecto y vaciaba la caché en cada petición. Si tú confías en las cabeceras del backend, sácalo.

expires 1y + add_header Cache-Control "public, no-transform"

Se lo dice al navegador del visitante, no a Nginx. El navegador deja de pedir ese asset durante un año. Combinado con el hash del nombre de archivo, es seguro: si el asset cambia, cambia el hash, cambia la URL, y el navegador lo pide sin enterarse del expires.

add_header X-Cache-Status $upstream_cache_status

Una cabecera chorra solo para depurar. Abre las DevTools (F12 → Red) y mira la respuesta: verás HIT, MISS, BYPASS o STALE. La primera carga de una página suele ser MISS; al refrescar debería pasar a HIT. Si no pasa, tienes un problema y esta cabecera te lo chiva.

4. Cómo saber que está funcionando

  1. Comprueba que la configuración sea correcta ejecutando sudo nginx -t.
  2. Recarga Nginx: sudo nginx -s reload o, si lo gestionas con systemd, sudo systemctl reload nginx.
  3. Abre la web y vete a la pestaña Red de las DevTools.
  4. La primera petición a un asset o una página debe llevar X-Cache-Status: MISS.
  5. Recarga la misma URL: ahora debería ser HIT.

5. Conclusión

Con una configuración básica de tu virtualhost, Nginx pasa de ser un simple reverse proxy a convertirse en un escudo de verdad para tu backend. Menos carga en el origen, respuestas casi instantáneas para el visitante y, sobre todo, la tranquilidad de que un despliegue, un reinicio o una conexión con el backend que falla no se traducen en una web caída. Es de esos cambios pequeños que, una vez los tienes, no entiendes cómo vivías sin ellos.