Cómo solucionar el error NGINX 502 Bad Gateway con Docker Proxy Pass

El error 502 Bad Gateway en NGINX es una de las incidencias más frecuentes y frustrantes al desplegar arquitecturas de microservicios en Docker. En términos de protocolo HTTP, este código significa una sola cosa: NGINX está funcionando correctamente como puerta de enlace, pero al intentar reenviar la petición hacia el contenedor backend mediante la directiva proxy_pass, recibió una respuesta inválida o no pudo establecer comunicación TCP.

A diferencia de un servidor tradicional en metal desnudo donde los servicios escuchan en 127.0.0.1, en Docker intervienen capas de aislamiento de red (network namespaces), cortafuegos de iptables y resolución DNS interna. Si estás implementando este despliegue sobre nuestra Guía Maestra de NGINX Reverse Proxy con Docker Compose y SSL, este artículo desglosa las 4 causas raíz del fallo y los comandos exactos para restablecer el servicio.

// EJECUCIÓN RÁPIDA (TL;DR) – LAS 3 COMPROBACIONES CLAVE

# 1. El error más común: NGINX y tu Backend NO están en la misma red Docker:

docker network inspect proxy-network | grep -E "nginx|mi-backend"

# 2. Nunca uses ‘localhost’ en proxy_pass; usa el nombre del contenedor y su puerto INTERNO:INCORRECTO: proxy_pass http://localhost:3000; | CORRECTO: proxy_pass http://mi-backend:3000;

# 3. Verificar si el backend está caído o en bucle de reinicio:

docker logs --tail 50 mi-backend

1. Inspección del Registro de Errores de NGINX (Error Log)

No intentes adivinar el fallo modificando configuraciones a ciegas. El registro de errores de NGINX describe de forma explícita el motivo del 502. Ejecuta en la terminal del host:

docker logs --tail 30 nginx-proxy

Identifica cuál de estos dos mensajes aparece en las últimas líneas:

  • Caso A: connect() failed (111: Connection refused) while connecting to upstream — NGINX encuentra la dirección IP del contenedor backend, pero no hay ningún proceso escuchando en el puerto destino o el contenedor se apagó.
  • Caso B: host not found in upstream "mi-backend" in /etc/nginx/conf.d/app.conf — NGINX no puede resolver el nombre del servicio porque no comparten la misma red bridge.

Causa 1: Aislamiento de Redes en Docker (Host not found)

Por defecto, si levantas NGINX en un docker-compose.yml y tu aplicación web (Node.js, Python, PHP-FPM) en otro archivo compose independiente sin declarar una red externa, Docker crea dos redes bridge aisladas (ej. nginx_default y app_default). Los contenedores no pueden comunicarse entre sí por nombre.

Solución: Unificar los contenedores en una red externa compartida

  1. Crea la red en el host si aún no existe: docker network create proxy-network
  2. En el docker-compose.yml de tu aplicación backend, agrégala a la red: services: mi-backend: image: mi-app:latest container_name: mi-backend networks: - proxy-networknetworks:
    proxy-network:
    external: true

  3. Reinicia el contenedor de la aplicación: docker compose up -d.

Causa 2: La Trampa de ‘localhost’ en la Directiva ‘proxy_pass’

En el archivo de configuración de NGINX (nginx.conf o conf.d/app.conf), muchos administradores configuran:

# CONFIGURACIÓN ERRÓNEA:
location / {
    proxy_pass http://localhost:3000;
}

Por qué falla: Dentro de un contenedor Docker, localhost o 127.0.0.1 apunta al propio espacio de red del contenedor de NGINX, no a tu máquina host ni al contenedor de tu aplicación. Como NGINX no tiene ningún servicio escuchando en el puerto 3000 dentro de su propio contenedor, devuelve de inmediato 502 Bad Gateway.

Configuración correcta: Debes utilizar el nombre del servicio definido en Compose o el nombre del contenedor (container_name):

# CONFIGURACIÓN CORRECTA:
location / {
    proxy_pass http://mi-backend:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

Nota sobre puertos: Debe ser el puerto interno en el que la aplicación escucha dentro del contenedor (ej. 3000), no el puerto mapeado en el host.

Causa 3: Caché DNS de NGINX tras Reinicios de Contenedores

El motor DNS de Docker asigna direcciones IP dinámicas a los contenedores dentro de la subred bridge (ej. 172.18.0.4). Cuando un contenedor backend se reinicia (por ejemplo, tras desplegar una nueva imagen), Docker puede asignarle una IP diferente (ej. 172.18.0.5).

Por diseño, NGINX resuelve los nombres de host del proxy_pass solo una vez durante el arranque y almacena la IP en caché para siempre. Si la IP del backend cambia, NGINX seguirá enviando tráfico a la IP vieja, generando un error 502 persistente.

Solución: Configurar el Resolver Interno de Docker

Para forzar a NGINX a consultar dinámicamente el servidor DNS integrado de Docker (que siempre opera en la IP 127.0.0.11), define una variable en la directiva proxy_pass:

server {
    listen 80;
    server_name api.tudominio.com;

    # DNS interno de Docker con tiempo de vida (TTL) de 10 segundos
    resolver 127.0.0.11 valid=10s;
    set $upstream_endpoint http://mi-backend:3000;

    location / {
        proxy_pass $upstream_endpoint;
        proxy_set_header Host $host;
    }
}

Causa 4: El Contenedor Backend Murió por Falta de Memoria

Si la configuración de red es correcta pero el 502 aparece de forma intermitente bajo picos de carga, el problema reside en el backend. Si el kernel de Linux se queda sin memoria RAM, invoca al proceso OOM-Killer para matar el contenedor de la aplicación.

Comprueba si el contenedor backend fue terminado abruptamente verificando su código de salida con el comando:

docker inspect mi-backend --format '{{.State.ExitCode}} - OOMKilled: {{.State.OOMKilled}}'

Si la salida retorna 137 - OOMKilled: true, tu aplicación colapsó por agotar la RAM asignada. Aplica de inmediato nuestra guía para diagnosticar y corregir el error OOMKilled (Exit Code 137).

Verificación Final y Recarga en Caliente

Una vez corregido el archivo de configuración o la red, valida la sintaxis de NGINX sin reiniciar el contenedor:

# Probar sintaxis
docker exec -it nginx-proxy nginx -t

# Recargar configuración sin pérdida de conexiones
docker exec -it nginx-proxy nginx -s reload

Si NGINX devuelve syntax is ok y test is successful, el error 502 desaparecerá de inmediato. Recuerda que si además gestionas certificados automáticos con Let’s Encrypt, debes mantener operativa la regla del endpoint ACME para evitar el error Certbot Challenge Failed for Domain.

Preguntas Frecuentes

¿Por qué curl dentro del contenedor NGINX devuelve Connection Refused?

Si ejecutas docker exec -it nginx-proxy wget -qO- http://mi-backend:3000 y falla, significa que la aplicación backend dentro de su contenedor está escuchando únicamente en 127.0.0.1 local en lugar de escuchar en todas las interfaces (0.0.0.0). En Node.js, Express o FastAPI, asegúrate de iniciar el servidor en host: '0.0.0.0'.

¿Influye ejecutar Docker sobre Proxmox VE en este error?

Si corres Docker dentro de contenedores LXC en Proxmox, debes asegurarte de que el contenedor LXC tenga habilitada la casilla de Nesting (anidamiento) y Keyctl en las opciones del hipervisor para que el puente de red virtual de Docker pueda gestionar las tablas de reenvío sin restricciones del kernel, tal como explicamos en nuestra Guía de virtualización en Proxmox.


Manual técnico de infraestructura DevOps redactado por Gustavo Dulcey, especialista en Despliegue de Microservicios, Redes Docker y Balanceo de Carga.

Deja un comentario