Diagnóstico en cinco minutos
sudo nginx -t
sudo tail -n 100 /var/log/nginx/error.log
sudo ss -ltnp
systemctl --failed Busca mensajes como connection refused, no such file, permission denied o upstream prematurely closed connection.
Backend detenido
Comprueba el servicio y sus logs:
sudo systemctl status miapp
sudo journalctl -u miapp --since '15 minutes ago'
curl -v http://127.0.0.1:3000/health Si curl local falla, corrige la aplicación antes de tocar Nginx.
Puerto o socket incorrecto
Compara proxy_pass o fastcgi_pass con ss -ltnp. En PHP-FPM confirma la versión y ruta del socket. Un upgrade puede cambiar php8.3-fpm.sock por otra versión.
Permisos y SELinux/AppArmor
Un socket existe pero Nginx puede no tener permiso. Revisa propietario, grupo y modo. No resuelvas con chmod 777. En distribuciones con SELinux, consulta auditoría y aplica una política específica.
Docker y DNS
Dentro de Compose, localhost apunta al contenedor actual, no al host ni a otro servicio. Usa el nombre del servicio y la misma red. Comprueba docker compose ps y docker compose logs.
Recursos agotados
OOM, límites de archivos o falta de workers pueden cerrar conexiones. Revisa CPU y RAM y logs del kernel.
Después de editar, valida y recarga:
sudo nginx -t && sudo systemctl reload nginx Leer el mensaje exacto de Nginx
El texto que acompaña al 502 suele señalar la capa rota. No basta con buscar el código en el access.log; abre el error.log alrededor de la misma hora:
sudo grep -n ' 502 ' /var/log/nginx/access.log | tail -20
sudo journalctl -u nginx --since '20 minutes ago' --no-pager
sudo tail -n 200 /var/log/nginx/error.log Mensajes comunes y su interpretación inicial:
| Mensaje | Significado probable |
|---|---|
connect() failed (111: Connection refused) | Nadie escucha en el puerto o la conexión se rechaza |
connect() to unix:... failed (2: No such file) | Socket ausente o ruta equivocada |
connect() to unix:... failed (13: Permission denied) | Nginx no puede atravesar directorios o abrir el socket |
upstream prematurely closed connection | El backend cerró antes de completar la respuesta |
no live upstreams | Todos los destinos se marcaron no disponibles |
SSL_do_handshake() failed | Protocolo o validación TLS entre Nginx y upstream |
host not found in upstream | DNS o nombre del servicio no resolvió al cargar configuración |
La misma página 502 puede representar cualquiera de estos fallos. Elegir la corrección por el mensaje evita aumentar timeouts cuando el puerto está vacío o cambiar permisos cuando la aplicación se cae por memoria.
Identificar qué bloque atiende el dominio
Antes de editar, confirma el server y location efectivos para esa solicitud. Nginx puede tener varios archivos, inclusiones y un sitio por defecto que recibe el dominio equivocado.
sudo nginx -T > /tmp/nginx-config.txt
grep -nE 'server_name|proxy_pass|fastcgi_pass|uwsgi_pass' /tmp/nginx-config.txt nginx -T valida y vuelca la configuración completa, incluidas inclusiones. Revisa qué server_name coincide y si una ubicación más específica sobrescribe la general. Una petición a /api/ puede usar otro upstream que /.
Comprueba DNS y encabezado Host:
dig +short tu-dominio.com A
curl -I -H 'Host: tu-dominio.com' http://127.0.0.1/ La segunda prueba evita depender de DNS externo y permite saber qué virtual host selecciona el Nginx local. Si funciona por IP pero no por dominio, el problema puede estar antes del proxy o en la selección del sitio.
Probar el upstream sin pasar por Nginx
La prueba decisiva es solicitar directamente la dirección que aparece en proxy_pass. Para una aplicación HTTP en loopback:
curl -v --max-time 10 http://127.0.0.1:3000/health
sudo ss -ltnp | grep ':3000' Si curl falla, Nginx no puede arreglar la aplicación. Si responde, replica encabezados relevantes:
curl -v --max-time 10 \
-H 'Host: tu-dominio.com' \
-H 'X-Forwarded-Proto: https' \
http://127.0.0.1:3000/ Algunos frameworks validan el host o generan redirecciones según protocolo. Una respuesta directa diferente ayuda a descubrir que el backend rechaza el encabezado, no que la red esté caída.
No uses solo ping: un host puede responder ICMP y no tener el puerto abierto. Tampoco uses el navegador como única prueba porque incorpora DNS, CDN, caché y TLS adicionales.
Recuperar un servicio systemd detenido
Cuando el puerto está vacío, revisa por qué el servicio terminó antes de iniciarlo de nuevo. Para una unidad miapp.service:
sudo systemctl status miapp --no-pager -l
sudo journalctl -u miapp --since '30 minutes ago' --no-pager
sudo systemctl show miapp -p ExecStart -p User -p WorkingDirectory -p EnvironmentFiles Los fallos frecuentes son ruta de trabajo incorrecta, variable ausente, archivo no legible, migración fallida o puerto ocupado. Verifica la configuración con el comando propio de la aplicación cuando exista.
Después de corregir, inicia y prueba localmente:
sudo systemctl restart miapp
systemctl is-active miapp
curl -fsS http://127.0.0.1:3000/health Solo recarga Nginx si también cambiaste su configuración. Reiniciar ambos servicios a la vez impide saber cuál corrección tuvo efecto.
Corregir una dirección o puerto que no coincide
proxy_pass debe señalar la dirección donde el proceso escucha realmente. Compara salida y configuración:
sudo ss -ltnp
sudo nginx -T | grep -n 'proxy_pass' Un backend que escucha en 127.0.0.1:3001 no recibirá tráfico dirigido a 127.0.0.1:3000. Si escucha solo en ::1, una dirección IPv4 también puede fallar. Haz coincidir ambos lados y conserva loopback para servicios locales.
Un bloque típico es:
location / {
proxy_pass http://127.0.0.1:3000;
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;
} La barra final en proxy_pass cambia cómo Nginx transforma la ruta en determinados bloques. Si el backend recibe una URL inesperada, compara proxy_pass http://...; con proxy_pass http://.../; y prueba la ruta resultante; no cambies al azar.
Resolver sockets Unix de PHP-FPM
Después de una actualización de PHP, Nginx puede seguir apuntando al socket de la versión anterior. Lista unidades y sockets:
systemctl list-units --type=service 'php*-fpm.service'
sudo find /run/php -maxdepth 1 -type s -ls
sudo nginx -T | grep -n 'fastcgi_pass' Una configuración coherente usa el socket que realmente crea el pool:
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
} El número de versión es solo un ejemplo; utiliza el instalado y soportado en tu servidor. Comprueba FPM:
sudo systemctl status php8.3-fpm --no-pager
sudo journalctl -u php8.3-fpm --since '30 minutes ago' Si el socket no existe, arregla el servicio. Si existe y el mensaje es permiso denegado, revisa usuario y grupo del pool, del socket y de los directorios del camino.
Reparar permisos sin usar chmod 777
Nginx necesita permiso para atravesar cada directorio y abrir el socket, no permiso total sobre toda la aplicación. Examina el camino:
namei -l /run/php/php8.3-fpm.sock
stat -c '%U:%G %a %n' /run/php/php8.3-fpm.sock
ps -o user,group,comm -C nginx Configura el pool para crear el socket con un grupo que incluya al worker de Nginx, por ejemplo:
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660 Después valida FPM según la versión y recarga el servicio. chmod 777 sobre el socket es temporal y se pierde al recrearlo; aplicado al árbol web, además permite escrituras que no son necesarias.
En sistemas con SELinux, permisos Unix correctos no garantizan acceso. Consulta denegaciones y aplica el tipo o booleano apropiado; no desactives SELinux para que desaparezca el síntoma.
Diagnosticar Node.js, Gunicorn y otros backends
Un proceso puede escuchar y aun así cerrar la conexión por una excepción. Revisa logs de la unidad y ejecuta una solicitud que reproduzca la ruta:
sudo journalctl -u miapp -f
curl -v http://127.0.0.1:3000/ruta-afectada Si /health funciona pero una ruta devuelve 502, la diferencia puede ser tamaño de respuesta, dependencia externa, consulta o error de aplicación. Activa un identificador de solicitud y regístralo en proxy y backend para seguir la misma operación.
Para Gunicorn, revisa número y reinicios de workers. Para Node, busca excepciones no capturadas y cierres por memoria. No conviertas un crash en un timeout enorme: estabiliza la aplicación, limita concurrencia y añade una ruta de salud que no dependa de trabajo costoso.
Resolver un 502 entre Nginx y Docker
127.0.0.1 dentro de un contenedor apunta al mismo contenedor, no al host ni a otro servicio. La dirección correcta depende de dónde corre Nginx:
- Nginx en el host y app publicada en loopback:
127.0.0.1:PUERTO_HOST. - Nginx y app en la misma red Compose:
NOMBRE_SERVICIO:PUERTO_INTERNO. - Nginx en contenedor y app solo publicada en host: rediseña o usa una ruta de host explícita compatible.
Comprueba redes y puertos:
docker compose ps
docker compose logs --tail=200 app
docker inspect app --format '{{json .NetworkSettings.Networks}}'
docker exec nginx getent hosts app En Compose no uses la IP efímera del contenedor. El nombre de servicio se actualiza al recrearlo; la IP codificada deja de ser válida. Asegura que proxy y app participan en una red común.
Un contenedor Up puede tener el proceso bloqueado. Añade healthcheck y prueba desde el contenedor de Nginx:
docker exec nginx wget -S -O- http://app:3000/health Corregir protocolo HTTP y HTTPS hacia el upstream
Si el backend espera TLS y proxy_pass usa HTTP, o al revés, la respuesta parece inválida y puede terminar en 502. Prueba ambos protocolos conscientemente:
curl -v http://127.0.0.1:8443/
curl -vk https://127.0.0.1:8443/ No uses -k como solución permanente; solo ayuda a distinguir protocolo de validación. Si Nginx habla HTTPS con un upstream por nombre, configura SNI y validación de certificado de acuerdo con tu red:
proxy_pass https://backend.ejemplo.internal;
proxy_ssl_server_name on;
proxy_ssl_name backend.ejemplo.internal;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; Desactivar la verificación oculta certificados vencidos o un destino incorrecto. En loopback controlado puedes tomar otra decisión, pero debe ser explícita y documentada.
Cuando el upstream cierra prematuramente
upstream prematurely closed connection indica que la conexión empezó y el backend la cerró antes de completar encabezados o cuerpo. Busca simultáneamente excepciones, OOM, reinicios y límites:
sudo journalctl -k --since '30 minutes ago' | grep -Ei 'oom|killed process'
sudo journalctl -u miapp --since '30 minutes ago'
sudo systemctl show miapp -p NRestarts -p MemoryCurrent -p MemoryMax También puede aparecer cuando una respuesta excede límites de encabezado o el backend tiene su propio timeout. No aumentes buffers sin medir el tamaño y entender por qué la aplicación genera cookies o encabezados enormes.
Si el problema ocurre solo en cargas de archivos, revisa client_max_body_size y límites de la aplicación. Un rechazo de tamaño suele ser 413, pero un backend que termina al procesar un archivo puede manifestarse como 502.
Revisar recursos, descriptores y colas
Un servidor sin memoria o sin descriptores puede rechazar conexiones aunque la configuración sea correcta. Captura:
free -h
vmstat 1 5
df -h
sudo ss -s
sudo systemctl show nginx -p LimitNOFILE
sudo systemctl show miapp -p LimitNOFILE -p TasksCurrent Revisa logs del kernel por OOM y del backend por too many open files. Aumentar límites solo es seguro después de corregir fugas de conexiones y dimensionar el consumo. Un pool de workers saturado puede aceptar TCP pero cerrar o rechazar solicitudes.
Correlaciona el número de 502 con tráfico y despliegues. Si comienza justo al cambiar versión, revertir puede ser una mitigación más segura que reiniciar repetidamente la nueva.
Aplicar y verificar una corrección de Nginx
Toda modificación debe pasar validación y una prueba funcional antes de cerrar el incidente. Guarda copia o usa control de versiones, edita un cambio y ejecuta:
sudo nginx -t
sudo systemctl reload nginx
systemctl is-active nginx
curl -fsS -o /dev/null -w '%{http_code} %{time_total}\n' \
https://tu-dominio.com/ruta-afectada Una recarga conserva conexiones mejor que un reinicio y rechaza configuraciones inválidas si validas primero. Comprueba además que una ruta no afectada continúa funcionando y observa el error.log mientras repites la solicitud.
Prevenir nuevos 502 durante despliegues
El proxy solo debe enviar tráfico a instancias que ya estén listas. Para reducir errores:
- Inicia la nueva versión en otro puerto.
- Ejecuta migraciones compatibles y prueba salud.
- Cambia el upstream y recarga Nginx.
- Conserva la versión anterior durante la observación.
- Registra reinicios y códigos 5xx por servicio.
En un único VPS no siempre habrá despliegue sin corte, pero puedes evitar el hueco entre detener el contenedor viejo y tener listo el nuevo. Healthchecks reales y un rollback probado convierten un 502 de varios minutos en un cambio reversible.
Encabezados demasiado grandes y respuestas incompletas
Un backend puede responder, pero producir encabezados que no caben en los buffers del proxy. El error.log mostrará mensajes relacionados con encabezados demasiado grandes. Antes de aumentar memoria, inspecciona qué envía la aplicación:
curl -sv -o /dev/null http://127.0.0.1:3000/ruta 2>&1 Cookies acumuladas, tokens duplicados o redirecciones con metadatos enormes suelen ser defectos de aplicación. Borrar cookies del navegador puede ocultarlo para una persona, pero no corrige la respuesta. Reduce los encabezados y solo ajusta buffers si el tamaño es legítimo y conocido.
Para respuestas grandes, Nginx usa buffering y archivos temporales según configuración. Un disco lleno o un directorio temporal sin permisos puede interrumpir el proxy. Revisa:
df -h
df -i
sudo nginx -T | grep -nE 'proxy_buffer|proxy_temp_path' No desactives proxy_buffering globalmente para resolver una ruta sin medir. Streaming, eventos y descargas tienen necesidades distintas; limita cambios a la ubicación afectada.
Crear una página de error sin esconder la avería
Una página 502 clara mejora la experiencia, pero debe conservar el código 502 para monitoreo y clientes. Puedes definir:
proxy_intercept_errors on;
error_page 502 /502.html;
location = /502.html {
root /var/www/errors;
internal;
} No devuelvas 200 con un mensaje de “mantenimiento”: buscadores, balanceadores y monitores creerán que el servicio está sano. Mantén la página estática, sin depender de la misma aplicación caída, e incluye una forma de reintentar sin revelar rutas internas.
Cerrar el incidente con causa y prueba
“Se reinició y volvió” describe una mitigación, no una causa. Registra el primer mensaje del error.log, el estado del upstream, el cambio aplicado y la prueba que confirmó recuperación. Si hubo OOM, conserva el evento del kernel; si hubo un socket viejo, registra qué actualización cambió la ruta.
Comprueba durante un periodo razonable:
sudo awk '$9 == 502 {count++} END {print count+0}' /var/log/nginx/access.log
sudo journalctl -u miapp --since '10 minutes ago' --no-pager
curl -fsSI https://tu-dominio.com/ruta-afectada El conteo total del archivo incluye incidentes antiguos; para monitoreo real filtra por tiempo o usa métricas. Añade una alerta por tasa de 502 y otra por reinicios del backend. Así el próximo fallo se detecta antes de que un usuario envíe una captura.
Upstreams con varias instancias y fallos parciales
Un grupo puede devolver 502 solo en algunas solicitudes si una de sus instancias está dañada. Revisa $upstream_addr y $upstream_status en el log para saber qué destino atendió cada petición. Sin esos campos, el error parece aleatorio.
log_format upstream_debug '$request_id $status '
'addr=$upstream_addr status=$upstream_status '
'connect=$upstream_connect_time response=$upstream_response_time'; Un grupo básico:
upstream miapp_backend {
server 127.0.0.1:3001 max_fails=3 fail_timeout=10s;
server 127.0.0.1:3002 max_fails=3 fail_timeout=10s;
} La comprobación pasiva necesita tráfico para detectar fallos y no garantiza que una instancia sea funcional antes de recibir su primera solicitud. Durante despliegues, prueba cada puerto y retira del grupo una instancia que no pase salud.
for port in 3001 3002; do
curl -fsS "http://127.0.0.1:$port/health" || echo "fallo en $port"
done No configures reintentos indiscriminados para solicitudes que crean pagos o pedidos. Repetir un POST tras un cierre ambiguo puede duplicar la operación si el backend la procesó antes de cortar. Diseña idempotencia en la aplicación y limita los métodos que el proxy puede reenviar de forma segura.
Revisar el archivo de resolución y la red del host
Cuando proxy_pass usa un nombre, cambios de DNS pueden dejar a Nginx con una dirección antigua o imposible de resolver. Comprueba desde el mismo host:
getent ahosts backend.ejemplo.internal
resolvectl query backend.ejemplo.internal
curl -v http://backend.ejemplo.internal:3000/health El comportamiento de resolución depende de si el nombre aparece en un upstream estático, variables y la configuración de resolver. No añadas una IP a /etc/hosts como arreglo permanente sin registrar quién la mantendrá; funciona hasta la siguiente migración y luego crea otro 502.
WebSocket y conexiones que necesitan Upgrade
Una aplicación puede funcionar por HTTP normal y fallar solo al abrir WebSocket si Nginx no reenvía los encabezados de actualización. El navegador mostrará desconexiones y el proxy puede registrar cierre o respuesta inválida. Configura la ubicación correspondiente:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /socket/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
} Coloca map en el contexto http, no dentro de location. Valida y prueba con un cliente WebSocket o la función real; curl -I no reproduce una conexión actualizada completa.
Si la conexión abre y cae después de un intervalo, revisa timeouts del proxy, aplicación y balanceador. No confundas un 502 durante el handshake con un 504 por inactividad. Registra la ruta y la instancia upstream, especialmente cuando varias réplicas mantienen estado de sesión.
Cambios de DNS durante una migración del backend
Mover el upstream a otra IP requiere coordinar resolución, salud y tiempo de caché. Baja el TTL antes de la ventana, prueba el nuevo backend con encabezado Host y conserva el anterior hasta observar tráfico sano.
curl -fsS -H 'Host: tu-dominio.com' http://IP_NUEVA:3000/health
getent ahosts backend.ejemplo.internal Si Nginx resolvió el nombre al iniciar, una modificación DNS puede no aplicarse como esperas hasta recargar o usar resolución dinámica configurada correctamente. Comprueba la versión y el patrón de upstream; no presupongas comportamiento.
Mantén una vuelta atrás que restaure dirección y aplicación. Eliminar el servidor antiguo al primer 200 deja sin salida si aparecen 502 solo en rutas con datos, sesiones o carga real.
Lee error.log, prueba el backend directamente y compara el upstream con el puerto o socket real. Un 502 se resuelve siguiendo la conexión de Nginx hacia la aplicación, no recargando servicios al azar.
Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.