Problema

En entornos donde Nginx actúa como reverse proxy para uno o varios contenedores Docker, es frecuente encontrarse con respuestas 502 Bad Gateway o logs que indican connect() failed (111: Connection refused) while connecting to upstream. El síntoma típico es que el cliente (navegador, curl, Cloudflare Tunnel) llega a Nginx, pero Nginx no puede establecer la conexión TCP con el backend. El error se manifiesta como:

2026/07/05 02:31:31 [error] 22#22: *7 connect() failed (111: Connection refused) while connecting to upstream, client: X.X.X.X, server: example.com, request: "GET / HTTP/1.1", upstream: "http://172.29.0.2:8086/", host: "media.example.com"

Este patrón aparece en cualquier stack que combine:

  • Docker bridge network o redes personalizadas.
  • Nginx configurado con proxy_pass http://service_name:port.
  • Servicios que escuchan en puertos diferentes a los expuestos en el contenedor.
  • Opciones de seguridad como KnownProxies o firewalls internos.

El problema no está limitado a Jellyfin; cualquier aplicación que dependa de un puerto interno distinto al mapeado en docker-compose.yml puede generar la misma falla.

Causa

1. Puerto interno incorrecto en proxy_pass

Nginx resuelve service_name mediante DNS interno de Docker y usa el puerto interno del contenedor. Si el proxy_pass apunta a 8086 pero el proceso escucha en 8096, la conexión será rechazada. En el log de ejemplo, Nginx intenta http://172.29.0.2:8086/ mientras Jellyfin está configurado para InternalHttpPort=8096.

2. Red de contenedores aislada o falta de alias

Si los contenedores están en una red personalizada (proxy-net) pero Nginx no está conectado a ella, la resolución DNS falla o la IP resultante pertenece a otra red. En ese caso, Nginx aún puede resolver el nombre (por la red default) pero la IP no corresponde al contenedor que ejecuta el servicio.

3. Mapeo de puertos incompleto o conflicto de puertos

Cuando el contenedor expone 8096:8096 pero el servicio interno está configurado para otro puerto, el mapeo externo es irrelevante para Nginx porque la comunicación ocurre dentro de la red bridge. Además, si otro proceso ocupa el puerto interno, el contenedor falla al iniciar y Nginx recibe una conexión rechazada.

4. Políticas de seguridad de la aplicación

Aplicaciones como Jellyfin pueden filtrar solicitudes basándose en la lista KnownProxies. Si la IP del contenedor Nginx no está incluida, la aplicación puede cerrar la conexión antes de responder, generando un Connection refused aparente.

5. Cloudflare Tunnel y encabezados de cliente

El túnel de Cloudflare introduce una capa adicional de IPs de origen. Si la aplicación confía en X-Forwarded-For y la IP del túnel no está marcada como proxy confiable, la petición puede ser rechazada antes de que Nginx la reenvíe.

Solución

Paso 1: Verificar puertos internos

  1. Accede al contenedor del servicio y ejecuta netstat -tlnp o ss -tln para confirmar el puerto de escucha.
  2. Ajusta la directiva proxy_pass para que apunte al puerto interno correcto. En el caso de Jellyfin:
# docker exec -it jellyfin bash
ss -tln | grep LISTEN
# salida esperada: LISTEN 0 128 0.0.0.0:8096

En nginx.conf cambia:

proxy_pass http://jellyfin:8096;

Paso 2: Asegurar que Nginx y el backend compartan la misma red

En docker‑compose.yml declara la red una sola vez y asigna ambos servicios:

networks:
  proxy-net:
    driver: bridge

services:
  nginx-proxy:
    ...
    networks:
      - proxy-net
  jellyfin:
    ...
    networks:
      - proxy-net

No mezcles redes default y proxy-net a menos que uses network_mode: service:nginx-proxy.

Paso 3: Revisar la configuración de KnownProxies (solo si la aplicación lo requiere)

Añade la subred de Docker (172.29.0.0/24 en el ejemplo) a la lista de proxies de la aplicación. En Jellyfin, el fragmento XML ya lo incluye, pero verifica que la subred coincida con la red actual (docker network inspect proxy-net).

Paso 4: Validar encabezados de Cloudflare

Si usas Cloudflare Tunnel, incluye la IP del túnel en KnownProxies o desactiva la validación estricta de X-Forwarded-For. En la mayoría de los casos, añadir 0.0.0.0/0 como set_real_ip_from en Nginx es suficiente, pero la aplicación también necesita reconocer esa IP como proxy.

Paso 5: Probar la cadena completa

Desde el host, ejecuta:

curl -v http://localhost:8096   # acceso directo al contenedor (bridge IP)
curl -v http://jellyfin:8096    # dentro de la red Docker (docker exec en nginx)
curl -v http://media.example.com   # a través de Nginx y Cloudflare

Cada paso debe devolver 200 OK. Si alguno falla, el mensaje de error indica dónde está el quiebre.

Paso 6 (opcional): Usar healthchecks

Añade un healthcheck a docker-compose.yml para que Docker marque el contenedor como saludable solo cuando el puerto interno responde:

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8096/health"]
  interval: 30s
  timeout: 5s
  retries: 3

Esto evita que Nginx reciba un contenedor en estado “starting” que aún no escucha.

Cuándo aplicar esta solución

  • Síntomas: 502 Bad Gateway, logs de Nginx con “Connection refused”, acceso directo al contenedor funciona pero a través de Nginx no.
  • Entorno: Docker Compose o Swarm con redes bridge personalizadas, Nginx como reverse proxy, cualquier aplicación que exponga puertos internos diferentes al externo.
  • Exclusiones: Si el error proviene de un firewall externo, de un balanceador de capa 7 distinto a Nginx, o de un problema de DNS externo, esta guía no cubre esos casos.

Código

# 1. Inspeccionar la red y obtener subred
docker network inspect proxy-net -f '{{json .IPAM.Config}}'

# 2. Ver puertos internos del contenedor
docker exec -it jellyfin ss -tln

# 3. Corregir nginx.conf (ejemplo)
cat > /share/Docker/externalaccess/nginx.conf <<'EOF'
events { worker_connections 1024; }
http {
    set_real_ip_from 0.0.0.0/0;
    real_ip_header X-Forwarded-For;
    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;

    server {
        listen 80;
        server_name media.mydomain.cc;
        location / {
            proxy_pass http://jellyfin:8096;
        }
    }
}
EOF

# 4. Reiniciar stack
docker compose down && docker compose up -d

Verificación

  1. Log de Nginx: docker logs nginx-proxy debe dejar de mostrar connect() failed (111: Connection refused).
  2. Respuesta HTTP: curl -I http://media.mydomain.cc debe devolver HTTP/1.1 200 OK.
  3. Healthcheck: docker inspect --format='{{json .State.Health}}' jellyfin debe mostrar Status: healthy.
  4. Cloudflare: Acceder desde internet y confirmar que la página carga sin errores 502.

Notas adicionales

  • Cuando cambies el puerto interno, actualiza también cualquier referencia en la configuración de la propia aplicación (por ejemplo, PublicHttpPort en Jellyfin) para evitar redirecciones incorrectas.
  • Si usas varios servicios detrás del mismo Nginx, crea bloques upstream con nombres claros y reutiliza la misma red para evitar colisiones de DNS.
  • En entornos con múltiples redes Docker, network_mode: bridge puede simplificar la resolución, pero pierde aislamiento; evalúa el trade‑off según tu política de seguridad.
  • Mantén los contenedores de proxy y backend en la misma subred para que la lista KnownProxies sea mínima y fácil de auditar.