Qué es Nginx en realidad
Nginx (pronunciado “engine-x”) es un servidor web construido sobre una arquitectura asíncrona y basada en eventos. En lugar de asignar un hilo o proceso por conexión, un pequeño número de procesos worker gestionan miles de conexiones simultáneamente reaccionando a eventos. Este diseño es la razón por la cual puede situarse delante de una aplicación lenta sin colapsar, y por qué se convirtió en la puerta de entrada predeterminada de la web moderna.
Desempeña tres roles que es fácil confundir. Es un servidor web estático, que devuelve archivos del disco sin problemas. Es un reverse proxy, que reenvía solicitudes a otro servidor y transmite la respuesta. Y es un load balancer, que distribuye las solicitudes entre un grupo de backends. Delante de una aplicación de Node.js, Python, Go o PHP, utilizarás los tres. Nginx no es parte de tu aplicación: es un proceso independiente con su propia configuración, ciclo de vida y logs.
Por qué colocarlo delante de tu aplicación
Podrías vincular Node directamente al puerto 80. Mucha gente lo hace en los tutoriales y funciona… hasta que deja de funcionar. Colocar Nginx delante te otorga un conjunto de capacidades que serían tediosas o peligrosas de implementar directamente en una aplicación.
Terminación TLS. Nginx gestiona el certificado, realiza el handshake y desencripta. Tu aplicación se comunica mediante HTTP simple en localhost y nunca tiene que gestionar la renovación de certificados. Certbot puede incluso reescribir la configuración por ti.
Archivos estáticos, compresión y almacenamiento en caché. Nginx sirve archivos con sendfile y kernel caching, comprime con gzip o brotli, y puede devolver una respuesta almacenada sin siquiera tocar el upstream.
Limitación de tasa (Rate limiting) y límites de solicitudes. El abuso se detiene en el borde, antes de que consuma el pool de conexiones o una consulta a la base de datos.
Un único punto de entrada. Una sola dirección pública y un certificado pueden dar servicio a múltiples servicios, enrutados por hostname o ruta. Puedes mover una aplicación a otro puerto o host sin cambiar el DNS.
Despliegues sin tiempo de inactividad (Zero-downtime deploys). Un upstream puede vaciar un backend mientras este se reinicia, y la recarga de la configuración es gradual (graceful). Node, expuesto directamente, corta todas las conexiones activas al reiniciarse.
El modelo de configuración
La configuración de Nginx es un árbol de contextos y directivas. Una directiva es un ajuste que termina en punto y coma; un contexto es un bloque encerrado entre llaves que contiene más directivas. Las directivas se heredan hacia abajo, y prevalece el contexto más específico.
# /etc/nginx/nginx.conf (main context)
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_types text/css application/javascript application/json;
# site configs are pulled in here
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Los contextos en los que trabajarás son:
- main — ajustes globales como
useryworker_processes. - events — cómo se gestionan las conexiones.
- http — todo lo relacionado con HTTP: tipos MIME, logging, compresión y los includes que importan los archivos del sitio.
- server — un host virtual, seleccionado mediante
listenyserver_name. - location — una ruta dentro de un server, donde se procesan realmente las solicitudes.
En Debian y Ubuntu, la convención es mantener un archivo por sitio en /etc/nginx/sites-available/ y crear enlaces simbólicos (symlinks) de los activos en /etc/nginx/sites-enabled/. Esto te ofrece una forma sencilla de desactivar un sitio sin tener que borrarlo.
sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app
sudo rm /etc/nginx/sites-enabled/app
Las directivas no son un lenguaje de scripting. No existen bucles ni variables en el sentido habitual — solo map, if (usados con moderación) e includes. Si sientes que un problema requiere flujo de control, la respuesta suele ser un map o un cambio en la aplicación.
Tu primer server block
Un bloque server es un host virtual. Escucha en un puerto, responde a uno o más nombres y contiene bloques location.
server {
listen 80;
server_name app.example.com www.app.example.com;
root /var/www/app/dist;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
server_name se compara con el encabezado Host. El primer server block de un puerto es el default server y captura las solicitudes cuyo host no coincida con nada más; por lo tanto, conviene configurarlo como un catch-all explícito que devuelva un error 444 o una página de mantenimiento, en lugar de exponer un sitio no deseado.
El orden de coincidencia es importante y suele causar confusiones. Los bloques location se prueban primero como coincidencias exactas (=), luego la coincidencia de prefijo más larga y, finalmente, las expresiones regulares (~ y ~*). Un prefijo ^~ le indica a Nginx que se detenga y utilice ese prefijo sin verificar las expresiones regulares. Cuando el comportamiento parece incorrecto, generalmente se debe a este orden.
location = /favicon.ico { log_not_found off; access_log off; }
location ^~ /assets/ { expires 1y; }
location ~* \.(jpg|png|css|js)$ { expires 30d; }
location / { try_files $uri /index.html; }
Proxying a aplicación Node.js
La directiva principal es proxy_pass. Apúntala hacia tu aplicación y Nginx se convertirá en un reverse proxy.
upstream app {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 30s;
proxy_connect_timeout 5s;
}
}
Definir un bloque upstream en lugar de escribir la dirección directamente no es solo para cuando tienes múltiples servidores. Es lo que te permite configurar keepalive, para que Nginx reutilice las conexiones al backend en lugar de abrir una nueva conexión TCP por cada solicitud. proxy_http_version 1.1 es necesario para el keepalive y para los WebSockets.
Los timeouts requieren atención. proxy_connect_timeout limita cuánto tiempo espera Nginx para establecer la conexión; proxy_read_timeout limita cuánto tiempo espera el siguiente byte desde el upstream. El read timeout debe ser superior a tu respuesta legítima más lenta, o de lo contrario Nginx devolverá un 504 mientras la aplicación aún esté procesando.
La regla de la barra final (trailing-slash) es el error clásico. proxy_pass http://app; reenvía la ruta completa sin cambios. proxy_pass http://app/; reemplaza el prefijo de la ubicación coincidente con /. Dentro de location /api/, el primero envía /api/users y el segundo envía /users. Elige con cuidado y revisa el log del upstream.
Reenvío de headers y por qué es importante para tu app
Por defecto, el upstream ve la solicitud como si proviniera de Nginx: la dirección del cliente es 127.0.0.1, el scheme es http y el header Host podría faltar. Esto rompe el logging, las redirecciones, las cookies y el rate limiting dentro de la app.
location / {
proxy_pass http://app;
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
Cada header tiene su razón de ser:
- Host preserva el dominio original, para que la app pueda construir URLs absolutas y virtual hosts correctos.
- X-Real-IP es la dirección inmediata del cliente.
- X-Forwarded-For se añade a una cadena;
$proxy_add_x_forwarded_formantiene las entradas existentes y añade el cliente actual. Usa la primera entrada como el cliente original y nunca confíes en ella ciegamente. - X-Forwarded-Proto le indica a la app si el usuario se conectó a través de HTTPS, lo que soluciona los bucles de redirección y los errores de secure-cookie.
- X-Forwarded-Host y X-Forwarded-Port son útiles cuando Nginx escucha en un puerto no estándar.
Debes configurar tu framework para que confíe en este proxy. En Express, app.set("trust proxy", 1) hace que req.ip y req.protocol lean los valores reenviados. Si omites esto, cada solicitud parecerá provenir de localhost, lo que romperá silenciosamente la geolocalización y el rate limiting por IP.
Sirviendo archivos estáticos y el fallback de la SPA
Nginx es excelente manejando archivos estáticos, así que deja que se encargue de esa tarea. Una división común es servir el frontend compilado desde el disco y hacer proxy únicamente para la API.
server {
listen 80;
server_name app.example.com;
root /var/www/app/dist;
location /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /api/ {
proxy_pass http://app;
proxy_set_header Host $host;
}
location / {
try_files $uri /index.html;
}
}
Para una aplicación de una sola página (SPA), try_files $uri /index.html es el fallback que permite que el enrutamiento del lado del cliente funcione: si la ruta solicitada no es un archivo, devuelve index.html y deja que el router lo gestione. Para una API, es mejor usar el mismo patrón con =404, ya que devolver HTML para un endpoint inexistente oculta errores.
Los assets que tienen hashes de contenido en sus nombres pueden cachearse indefinidamente: immutable y una expiración de un año son seguros porque cualquier cambio genera un nuevo nombre de archivo. index.html nunca debe cachearse de esa manera, o los usuarios seguirán cargando una versión antigua de la aplicación.
TLS con Let’s Encrypt y certbot
Certbot obtiene certificados gratuitos de Let’s Encrypt y puede configurar Nginx automáticamente. Instala el plugin, ejecútalo una vez por dominio y él se encargará del resto.
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com -d www.app.example.com
sudo certbot renew --dry-run
Certbot escribe las rutas de los certificados en tu server block e instala un temporizador de renovación. Los certificados duran 90 días; la renovación es automática, pero solo funciona si la verificación de renovación puede alcanzar tu servidor a través del puerto 80, así que no lo bloquees completamente con el firewall.
El bloque resultante se ve así, con un servidor HTTP que redirecciona y un servidor HTTPS que termina la conexión.
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
TLSv1.2 y TLSv1.3 son los únicos protocolos que vale la pena habilitar hoy en día. HSTS indica a los navegadores que rechacen el HTTP simple durante un año, así que actívalo solo cuando estés seguro de que HTTPS funciona en todas partes; es difícil de revertir.
HTTP/2 y HTTP/3
HTTP/2 multiplexa múltiples solicitudes a través de una sola conexión y se habilita por servidor. En versiones modernas de Nginx (1.25.1 y posteriores), la directiva es http2 on;. En versiones anteriores, es un flag en la línea listen: listen 443 ssl http2;.
server {
listen 443 ssl;
http2 on;
# ...
}
HTTP/3 se ejecuta sobre QUIC y UDP. Requiere una compilación con el módulo QUIC, una línea listen 443 quic reuseport; independiente y un encabezado Alt-Svc: h3=":443"; ma=86400 para anunciarlo. Considéralo como una optimización para añadir una vez que HTTP/2 sea estable, no como un primer paso. Los navegadores negocian ambos automáticamente a través de TLS, por lo que no hay nada que cambiar en tu app.
Métodos de balanceo de carga
Un bloque upstream define un pool, y el método de balanceo decide qué servidor recibe la siguiente solicitud. El valor predeterminado es round-robin.
upstream app {
least_conn;
server 10.0.0.11:3000 weight=2 max_fails=3 fail_timeout=15s;
server 10.0.0.12:3000;
server 10.0.0.13:3000 backup;
keepalive 64;
}
- Round-robin (predeterminado) rota entre los servidores. Es simple y equitativo cuando las solicitudes tienen un costo similar.
least_connenvía la solicitud al servidor con menos conexiones activas. Es mejor cuando la duración de las solicitudes varía considerablemente, algo común en las API.ip_hashvincula un cliente a un servidor específico mediante el hash de la dirección. Es útil para sesiones en memoria, aunque a costa de una carga desigual y la pérdida de persistencia si un servidor falla.weightsesga el tráfico hacia una máquina más potente o durante un despliegue canary.backupmarca un servidor para que solo reciba tráfico cuando los primarios estén caídos.
max_fails y fail_timeout proporcionan un chequeo de salud pasivo: tras max_fails fallos dentro de fail_timeout, Nginx marca el servidor como caído durante ese intervalo. proxy_next_upstream decide entonces qué fallos se reintentan en otro backend.
proxy_next_upstream error timeout http_502 http_503 http_504;
Reintentar es potente pero no es gratuito: solo tiene sentido para solicitudes idempotentes. No reintentes a ciegas un POST que pueda haber cambiado ya el estado, a menos que el upstream sea idempotente por diseño.
Health checks y manejo de fallos
Nginx OSS cuenta con comprobaciones pasivas; las comprobaciones de salud activas (health_check con match) requieren Nginx Plus. Las comprobaciones pasivas junto con un endpoint a nivel de aplicación suelen ser suficientes para la mayoría de los equipos: expón /healthz que verifique las dependencias necesarias —base de datos, caché— y responda rápidamente, y luego apunta la liveness probe de tu plataforma hacia él. Úsalo para controlar los despliegues iniciando un nuevo backend, esperando hasta que esté saludable y luego eliminando el anterior del upstream.
Dado que las recargas de configuración son graduales (graceful), el patrón para lograr cero tiempo de inactividad consiste en editar el upstream para que apunte a la nueva instancia, nginx -t, recargar y luego vaciar (drain) y detener la instancia antigua.
Almacenamiento en caché con proxy_cache
proxy_cache almacena las respuestas del upstream en disco y las sirve sin contactar al backend. Para los endpoints con alta carga de lectura que cambian lentamente, esta es la mejora individual más significativa que se puede implementar en el edge.
proxy_cache_path /var/cache/nginx levels=1:2
keys_zone=app:10m max_size=1g inactive=60m;
server {
listen 80;
server_name app.example.com;
location /api/public/ {
proxy_pass http://app;
proxy_cache app;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 302 10s;
proxy_cache_valid 404 1m;
proxy_cache_bypass $http_authorization $cookie_session;
proxy_no_cache $http_authorization;
add_header X-Cache-Status $upstream_cache_status;
}
}
proxy_cache_valid define cuánto tiempo se almacena en caché cada código de estado. proxy_cache_bypass omite la caché para las solicitudes que llevan un encabezado de sesión o de autorización; proxy_no_cache evita almacenar sus respuestas, asegurando que los datos privados de un usuario nunca se sirvan a otro. El encabezado X-Cache-Status (HIT, MISS, BYPASS, EXPIRED) es la forma más rápida de comprobar que la caché funciona.
Dos advertencias. Almacena en caché únicamente las respuestas que sea seguro compartir —datos públicos y no personalizados— y asegúrate de que el upstream envíe un encabezado Cache-Control compatible con tu configuración. Una clave de caché que ignore una variante (un encabezado de idioma, un parámetro de consulta) servirá alegremente el contenido incorrecto.
Rate limiting con limit_req
El rate limiting protege los endpoints de login, las búsquedas y las APIs públicas contra el abuso y las inundaciones accidentales de tráfico. Se define en el contexto http y se aplica en una location.
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_status 429;
server {
listen 80;
server_name app.example.com;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app;
}
location = /api/login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://app;
}
}
La clave de la zona suele ser $binary_remote_addr (la IP del cliente), y la tasa se define en peticiones por segundo o por minuto. burst permite picos cortos de tráfico, y nodelay procesa el burst inmediatamente en lugar de espaciarlo, que es lo ideal para endpoints interactivos. Sin nodelay, las peticiones excedentes se retrasan, lo que puede hacer que la UI se sienta lenta en lugar de simplemente devolver un 429.
Cuando hay otro proxy o un CDN delante, $binary_remote_addr puede ser la dirección del proxy, por lo que el limitador tratará a todos los usuarios como un único cliente. Soluciona esto configurando set_real_ip_from y real_ip_header para que Nginx vea al cliente real. Además, devuelve 429 en lugar del 503 predeterminado para que los clientes puedan distinguir el rate limiting de una caída del servicio.
Upgrade de WebSocket
Una conexión WebSocket comienza como una solicitud HTTP con un encabezado Upgrade y luego se convierte en un flujo bidireccional de larga duración. Para realizar el proxy de esta conexión, se necesitan los encabezados de upgrade y un valor Connection que depende de la solicitud, por lo que se utiliza un map.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
location /ws/ {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
El timeout de lectura prolongada es importante: un WebSocket inactivo con un timeout corto es cerrado por Nginx, y el cliente experimenta una desconexión misteriosa. Ten en cuenta que HTTP/2 no transporta WebSocket de la misma manera; los navegadores abren una conexión HTTP/1.1 independiente para el upgrade, la cual Nginx gestiona automáticamente en el mismo puerto.
Ejecutando Nginx en Docker
La imagen oficial de nginx es una forma conveniente de distribuir la configuración y los archivos estáticos con tu aplicación. La única regla es que el proceso principal del contenedor debe permanecer en primer plano, por lo que el comando termina con daemon off;.
# Dockerfile
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/app.conf
COPY dist/ /usr/share/nginx/html/
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]
En Compose, el servicio de la aplicación es accesible mediante su nombre de servicio, por lo que el host upstream es simplemente app. Nginx inicia antes de que la aplicación esté lista, así que puedes añadir un healthcheck y hacer que dependa de él, o dejar que Nginx reintente la conexión.
# docker-compose.yml
services:
nginx:
image: nginx:1.27-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/app.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
app:
condition: service_healthy
app:
build: .
expose:
- "3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
interval: 5s
retries: 10
Ten en cuenta el uso de expose en lugar de ports en la aplicación: esta solo debe ser accesible dentro de la red de Compose, nunca directamente desde el host. El upstream de la configuración de Nginx pasa a ser server app:3000;.
Pruebas, recarga y cambios de configuración sin tiempo de inactividad
Cada cambio de configuración debe seguir el mismo ciclo: validar, recargar y verificar. Una recarga (reload) es gradual; un reinicio (restart) no lo es.
sudo nginx -t # test the configuration
sudo nginx -T # test and dump the full effective config
sudo systemctl reload nginx # graceful, no dropped connections
sudo systemctl status nginx
curl -I https://app.example.com
nginx -T es una herramienta infrautilizada y extremadamente útil: imprime la configuración combinada exactamente como la ve Nginx, lo que resuelve cualquier duda sobre la herencia y los includes. Si -t falla, el error indica el archivo y la línea exacta.
Al recargar, mantén una terminal abierta en una segunda sesión para que una configuración errónea no te deje fuera del sistema. En un contenedor, el equivalente es enviar SIGHUP al proceso maestro o ejecutar nginx -s reload. Nunca reinicies un Nginx de producción con tráfico para aplicar un cambio de configuración cuando una recarga es suficiente.
Mejores prácticas
- Ejecuta
nginx -tantes de cada recarga; nunca edites y reinicies a ciegas. - Mantén un archivo por sitio y actívalo mediante un symlink para que los cambios sean reversibles.
- Define un bloque
upstreamconkeepaliveen lugar de escribir la dirección del backend directamente (inline). - Reenvía siempre
Host,X-Real-IP,X-Forwarded-ForyX-Forwarded-Proto, y configuratrust proxyen la aplicación. - Sirve los assets estáticos y los bundles hasheados desde el disco con cabeceras de caché largas e inmutables.
- Redirige HTTP a HTTPS y activa HSTS solo después de haber comprobado que HTTPS funciona correctamente.
- Almacena en caché únicamente respuestas públicas y no personalizadas, y nunca crees una clave de caché sin sus variantes.
- Implementa rate limit en el login y en los endpoints públicos, y devuelve un 429 en lugar de un 503.
- Utiliza timeouts largos para las ubicaciones de WebSocket y cortos para los health checks.
- Mantén los secretos y certificados fuera de las imágenes; móntalos en modo lectura durante el runtime.
Errores comunes
- Omitir
nginx -ty tirar el sitio con un error de sintaxis. - Olvidar
X-Forwarded-Protoy terminar depurando un bucle infinito de redirecciones HTTPS. - No habilitar
trust proxyen la aplicación, haciendo que cada solicitud parezca provenir de localhost. - Confundir
proxy_passcon y sin la barra final (trailing slash) y recibir errores 404. - Configurar
proxy_read_timeoutcon un tiempo menor que una respuesta lenta legítima y devolver errores 504. - Cachear respuestas personalizadas y servir los datos de un usuario a otro.
- Hacer proxy de WebSockets sin el
mapde upgrade, provocando que las conexiones se caigan inmediatamente. - Dejar el servidor por defecto como el primer bloque de sitio y exponer una aplicación no deseada.
- Habilitar HSTS antes de que HTTPS funcione en todas partes y bloquear el acceso a los usuarios durante un año.
- Exponer el servicio de la aplicación con
portsen Compose en lugar deexpose.
Próximos pasos
Nginx es la puerta de entrada, y la guía de HTTP / HTTPS explica el protocolo que termina y redirige. Para ejecutarlo junto a tu aplicación de forma reproducible, lee Docker & Deployment, y para mantener saludable el host donde se ejecuta, vuelve a consultar Linux. Cuando estés listo para lanzarlo a internet con certificados, DNS y autoscaling, la guía de Cloud Deployment une todas las piezas.