Saltar al contenido
Server 2026-09-05 22 min de lectura

Error 502 Bad Gateway en Nginx: causas y solución

Nginx devuelve 502 cuando actúa como proxy y no obtiene una respuesta válida del backend. La página de error es el síntoma; la causa suele estar en Node.js, PHP-FPM, Gunicorn, un contenedor o la dirección configurada.

Error 502 Bad Gateway en Nginx: causas y solución
#Nginx#Error 502#VPS#Troubleshooting
T
Equipo Terranode
Editorial

Diagnóstico en cinco minutos

bash
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:

bash
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:

bash
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:

bash
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:

MensajeSignificado 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 connectionEl backend cerró antes de completar la respuesta
no live upstreamsTodos los destinos se marcaron no disponibles
SSL_do_handshake() failedProtocolo o validación TLS entre Nginx y upstream
host not found in upstreamDNS 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.

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

nginx
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:

bash
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:

nginx
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:

bash
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:

bash
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:

ini
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:

bash
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:

bash
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:

bash
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:

bash
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:

nginx
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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

nginx
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:

bash
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.

nginx
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:

nginx
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.

bash
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:

bash
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:

nginx
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.

bash
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.

¿Listo para llevar tu sitio al siguiente nivel?

Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.

Ver planes de hosting

Preguntas frecuentes