La caché de proxy de Nginx guarda en disco las respuestas de tu aplicación y las sirve directamente a las siguientes peticiones, sin volver a llamar al backend. Funciona con cualquier servicio HTTP (Node.js, Python, Go, Java) y reduce tanto la latencia como la carga del servidor de aplicación. En este tutorial configurarás Nginx en Ubuntu 24.04 como reverse proxy con caché, lo harás resistente a caídas del backend, excluirás a los usuarios autenticados y medirás la tasa de aciertos.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • Nginx instalado (sudo apt install nginx).
  • Una aplicación HTTP escuchando en 127.0.0.1:3000. Si todavía no tienes una, el paso 1 levanta un backend de prueba.
  • Opcional para HTTPS: un dominio (your_domain) con un registro DNS A apuntando a la IP del servidor.

Paso 1: Levantar un backend de prueba

Si ya tienes tu aplicación en el puerto 3000, salta este paso. Para seguir la guía sin una aplicación real, sirve un directorio con el servidor HTTP incluido en Python:

mkdir -p ~/backend-demo
echo '<h1>Hola desde el backend</h1>' > ~/backend-demo/index.html
python3 -m http.server 3000 --bind 127.0.0.1 --directory ~/backend-demo

Deja esta terminal abierta y abre otra sesión SSH para el resto de pasos. Comprueba que responde:

curl -s http://127.0.0.1:3000/
<h1>Hola desde el backend</h1>

Paso 2: Definir la zona de caché

La zona de caché se declara una sola vez en el contexto http. En Ubuntu, todo lo que pongas en /etc/nginx/conf.d/ se carga dentro de ese contexto, así que crea allí un archivo:

sudo nano /etc/nginx/conf.d/proxy-cache.conf
proxy_cache_path /var/cache/nginx/app
    levels=1:2
    keys_zone=app_cache:20m
    max_size=5g
    inactive=24h
    use_temp_path=off;

log_format cache '$remote_addr [$time_local] "$request" $status '
                 '$body_bytes_sent cache=$upstream_cache_status rt=$request_time';

Qué significa cada parámetro:

  • levels=1:2 reparte los archivos en dos niveles de subdirectorios para no tener miles de archivos en una misma carpeta.
  • keys_zone=app_cache:20m reserva 20 MB de memoria compartida para las claves. Cada megabyte almacena unas 8.000 claves.
  • max_size=5g es el tamaño máximo en disco. Al superarlo, Nginx borra las entradas menos usadas.
  • inactive=24h elimina las entradas que nadie pide en 24 horas, aunque sigan siendo válidas.
  • use_temp_path=off escribe directamente en el directorio de caché, sin copiar desde un directorio temporal.

El formato de log cache añade el estado de caché de cada petición, que usarás en el paso 6 para calcular la tasa de aciertos. Nginx crea el directorio /var/cache/nginx/app al arrancar.

Paso 3: Configurar el reverse proxy con caché

Crea el bloque de servidor del sitio:

sudo nano /etc/nginx/sites-available/app
upstream app_backend {
    server 127.0.0.1:3000;
    keepalive 16;
}

server {
    listen 80;
    listen [::]:80;
    server_name your_domain;

    access_log /var/log/nginx/app-access.log cache;

    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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_cache app_cache;
        proxy_cache_key "$scheme$host$request_uri";
        proxy_cache_valid 200 301 302 10m;
        proxy_cache_valid 404 1m;

        add_header X-Cache-Status $upstream_cache_status always;
    }
}

Los puntos importantes:

  • proxy_cache_key define qué peticiones se consideran la misma. Incluir $host evita mezclar respuestas si el mismo Nginx sirve varios dominios. $request_uri incluye la query string, así que /productos?page=1 y /productos?page=2 se cachean por separado.
  • proxy_cache_valid fija cuánto tiempo se guarda cada código de estado cuando el backend no envía Cache-Control o Expires. Si el backend los envía, Nginx los respeta.
  • Nginx no cachea por defecto las respuestas que contienen Set-Cookie ni las que llevan Cache-Control: private, no-store o no-cache.
  • La cabecera X-Cache-Status te permite ver desde el cliente si la respuesta vino de la caché.

Activa el sitio, desactiva el sitio por defecto si no lo usas, valida la configuración y recarga:

sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Pide la misma URL dos veces. Sustituye your_domain por tu dominio, o usa localhost con -H 'Host: your_domain':

curl -s -o /dev/null -D - http://your_domain/ | grep -i x-cache-status
curl -s -o /dev/null -D - http://your_domain/ | grep -i x-cache-status
X-Cache-Status: MISS
X-Cache-Status: HIT

La primera petición llega al backend (MISS) y la segunda se sirve desde disco (HIT). Si paras el backend de prueba con Ctrl+C, la página seguirá respondiendo con HIT durante los 10 minutos de validez.

Paso 4: No cachear usuarios autenticados

Cachear una página personalizada y servírsela a otro usuario es el error más grave que se puede cometer con una caché. Nginx no mira la cabecera Authorization ni las cookies por sí solo, así que debes decirle cuándo saltarse la caché.

Edita el bloque location / del sitio:

sudo nano /etc/nginx/sites-available/app

Añade estas líneas debajo de proxy_cache_valid. Sustituye sessionid por el nombre de la cookie de sesión de tu aplicación:

        proxy_cache_bypass $http_authorization $cookie_sessionid;
        proxy_no_cache $http_authorization $cookie_sessionid;

proxy_cache_bypass hace que esas peticiones no se sirvan desde la caché y proxy_no_cache impide que su respuesta se guarde. Cualquier valor no vacío en las variables activa la condición. Las peticiones que no son GET ni HEAD nunca se cachean.

Si tu aplicación tiene zonas que nunca deben cachearse, como /admin/ o /api/, lo más claro es darles su propio location sin proxy_cache:

    location /admin/ {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

Valida y recarga con sudo nginx -t && sudo systemctl reload nginx. Comprueba que una petición con cabecera de autorización ya no usa la caché:

curl -s -o /dev/null -D - -H 'Authorization: Bearer test' http://your_domain/ | grep -i x-cache-status
X-Cache-Status: BYPASS

Paso 5: Servir contenido obsoleto y evitar avalanchas al backend

Dos problemas típicos de una caché en producción son las caídas del backend y la avalancha de peticiones idénticas cuando una entrada muy popular caduca. Nginx resuelve ambos con unas pocas directivas.

Edita de nuevo el sitio:

sudo nano /etc/nginx/sites-available/app

Añade al bloque location /, debajo de las líneas anteriores:

        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_revalidate on;
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;
  • proxy_cache_use_stale sirve la copia caducada si el backend devuelve error, no responde o la entrada se está actualizando.
  • proxy_cache_background_update on actualiza en segundo plano las entradas caducadas mientras el cliente recibe la copia anterior (STALE o UPDATING), sin esperar al backend.
  • proxy_cache_revalidate on usa peticiones condicionales (If-Modified-Since, If-None-Match) para renovar entradas, de modo que el backend puede responder 304 sin reenviar el cuerpo.
  • proxy_cache_lock on hace que, cuando varias peticiones buscan la misma entrada que no está en caché, solo una llegue al backend y las demás esperen hasta 5 segundos a que se rellene.

Valida y recarga:

sudo nginx -t && sudo systemctl reload nginx

Para probarlo, reduce temporalmente proxy_cache_valid 200 a 10s, recarga, pide la página, para el backend y espera 15 segundos. La siguiente petición seguirá devolviendo la página:

X-Cache-Status: STALE

Vuelve a dejar el valor original cuando termines la prueba.

Paso 6: Medir la tasa de aciertos

El log del sitio registra el estado de caché de cada petición gracias al formato cache del paso 2. Cuenta cuántas peticiones hay de cada tipo:

sudo grep -o 'cache=[A-Z-]*' /var/log/nginx/app-access.log | sort | uniq -c | sort -rn
   8412 cache=HIT
   1175 cache=MISS
    301 cache=BYPASS
     42 cache=-
     17 cache=STALE

cache=- corresponde a peticiones que no pasaron por la caché (por ejemplo, las de /admin/). Una tasa de HIT baja suele indicar que el backend envía Set-Cookie o Cache-Control: private en páginas que podrían cachearse, o que la clave incluye parámetros que cambian en cada petición, como identificadores de campañas.

Para ver el espacio ocupado en disco:

sudo du -sh /var/cache/nginx/app

Paso 7: Refrescar el contenido cacheado

La versión libre de Nginx no incluye una orden para purgar una URL concreta. Tienes dos alternativas sin módulos adicionales.

Vaciar toda la caché. Borra los archivos; Nginx reconstruye las entradas a medida que llegan peticiones:

sudo find /var/cache/nginx/app -type f -delete

Forzar la actualización de una URL. Permite que ciertas IP de confianza envíen una cabecera que salta la caché y guarda la respuesta nueva. Añade al archivo /etc/nginx/conf.d/proxy-cache.conf:

geo $refresh_allowed {
    default     0;
    127.0.0.1   1;
}

map "$refresh_allowed:$http_x_cache_refresh" $cache_refresh {
    default 0;
    "1:1"   1;
}

Y en el bloque location / del sitio, añade $cache_refresh a la línea existente de proxy_cache_bypass:

        proxy_cache_bypass $http_authorization $cookie_sessionid $cache_refresh;

Recarga Nginx. Desde el propio servidor, esta petición obtiene la página del backend y sobrescribe la entrada en caché:

curl -s -o /dev/null -D - -H 'Host: your_domain' -H 'X-Cache-Refresh: 1' http://127.0.0.1/ | grep -i x-cache-status
X-Cache-Status: BYPASS

La petición siguiente, sin la cabecera, devolverá HIT con el contenido nuevo. Las peticiones con esa cabecera desde cualquier otra IP se ignoran.

Paso 8: Añadir HTTPS

Con el dominio apuntando al servidor, abre los puertos web y obtén un certificado de Let's Encrypt con Certbot, que modifica el bloque server automáticamente:

sudo ufw allow 'Nginx Full'
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your_domain

La caché funciona igual sobre HTTPS. Como la clave incluye $scheme, las respuestas HTTP y HTTPS se guardan por separado; tras la redirección a HTTPS que añade Certbot, solo se usará la segunda.

Solución de problemas

Siempre aparece MISS. Revisa las cabeceras que envía el backend con curl -sI http://127.0.0.1:3000/. Set-Cookie, Cache-Control: private, no-store o no-cache y Vary: * impiden el cacheo. Corrige el backend si esas páginas son públicas; si no puedes, proxy_ignore_headers Cache-Control Set-Cookie; fuerza el cacheo, pero úsalo solo en rutas donde tengas claro que la respuesta es igual para todos los usuarios.

No aparece la cabecera X-Cache-Status. Si otro add_header está definido en un nivel más interno (por ejemplo en location), Nginx ignora los add_header de niveles superiores. Repite la línea en ese bloque.

Error mkdir() "/var/cache/nginx/app" failed. El directorio padre no existe o no es accesible. Créalo con sudo mkdir -p /var/cache/nginx y vuelve a ejecutar sudo nginx -t.

Se muestra contenido de un usuario a otro. Detén el problema vaciando la caché (paso 7) y revisa las condiciones del paso 4: la cookie de sesión debe coincidir exactamente con el nombre que usa tu aplicación.

Conclusión

Ahora Nginx cachea las respuestas públicas de tu backend, excluye a los usuarios autenticados, sigue sirviendo contenido cuando la aplicación falla y te permite medir y refrescar la caché. Como siguientes pasos puedes hacer que la aplicación envíe Cache-Control: public, max-age=... en cada ruta para controlar la duración desde el código, añadir un segundo servidor al bloque upstream para balancear carga o combinar esta caché con una CDN delante para los recursos estáticos.