Problema

En entornos de desarrollo o homelab es frecuente ejecutar Nginx Proxy Manager (NPM) dentro de Docker mientras que la aplicación que se desea exponer, como Jellyfin, corre directamente sobre Windows. Cuando el contenedor de NPM intenta enrutar el tráfico al host, el cliente recibe un 502 Bad Gateway y los logs indican connect() failed (111: Connection refused) while connecting to upstream "http://<IP>:8096/". El síntoma es idéntico aunque la IP usada sea la local del equipo, host.docker.internal o cualquier alias configurado. El problema no es DNS externo ni puertos del router; la falla ocurre dentro del stack de red de Docker/WSL2.

Causa

1. Segmentación de redes entre Docker y el host Windows

Docker Desktop en Windows utiliza una VM basada en WSL2. Cada contenedor se conecta a una red virtual (bridge) que no tiene rutas automáticas hacia la pila de red del host Windows. Por defecto, la única ruta conocida es host.docker.internal, que apunta a la interfaz de la VM, no al adaptador de red físico del host. Si la aplicación escucha solo en la IP de la LAN (por ejemplo 192.168.1.10), el contenedor no tiene una ruta directa y la conexión es rechazada.

2. Escucha restringida a localhost o a la interfaz de Windows

Muchas servicios instalados de forma nativa (Jellyfin, Plex, etc.) están configurados para escuchar únicamente en 127.0.0.1. Desde el contenedor esa dirección corresponde al propio contenedor, no al host, provocando el error de conexión. Cambiar la escucha a 0.0.0.0 o a la IP de la LAN es necesario.

3. Firewall de Windows bloqueando tráfico inter‑VM

Incluso con la regla de puerto abierta, el firewall puede filtrar paquetes que provienen de la subred virtual de WSL2 (172.28.0.0/16). Si la regla está limitada a “Domain/Private/Public” sin incluir la subred, la petición es descartada antes de llegar al proceso.

4. Configuración de red Docker personalizada

Al crear la red bridge con opciones de aislamiento (--internal) o al usar network_mode: host en Windows, el contenedor pierde la capacidad de salir a la red externa. En esos casos, cualquier intento de conectar al host falla con “connection refused”.

Solución

Paso 1 – Verificar la dirección de escucha del servicio

Accede a la configuración de Jellyfin (o la aplicación objetivo) y asegura que el bind address sea 0.0.0.0 o la IP de la LAN. En Windows esto suele estar bajo Network > Bind to address. Reinicia el servicio después de cambiarlo.

Paso 2 – Probar conectividad desde dentro del contenedor

Ejecuta una shell temporal en el contenedor de NPM y usa curl o nc para validar la ruta:

docker exec -it npm bash
curl -v http://host.docker.internal:8096

Si la respuesta es Connection refused, la causa está en la capa de red, no en NPM.

Paso 3 – Añadir una ruta estática a la subred del host

Dentro de la VM WSL2, crea una regla de iptables que redirija el tráfico destinado a la IP de la LAN hacia la interfaz de Windows:

sudo ip route add 192.168.0.0/16 via $(ip route | grep default | awk '{print $3}')

Esta línea asegura que cualquier paquete con destino a la red local salga de la VM a través del gateway de Windows.

Paso 4 – Exponer el host mediante --add-host

Si prefieres usar un nombre estático en NPM, modifica el docker-compose.yml de NPM añadiendo:

services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    extra_hosts:
      - "windows-host:host.docker.internal"

Luego, en la configuración de Proxy Host de NPM, usa http://windows-host:8096 como Forward Hostname / IP. Docker traducirá windows-host a la IP correcta de la VM, evitando errores de resolución.

Paso 5 – Ajustar el firewall de Windows

Crea una regla que permita tráfico entrante desde la subred 172.28.0.0/16 al puerto de la aplicación:

New-NetFirewallRule -DisplayName "Docker to Jellyfin" -Direction Inbound -Action Allow `
-Protocol TCP -LocalPort 8096 -RemoteAddress 172.28.0.0/16

Asegúrate de que la regla esté habilitada tanto en perfiles Domain como Private y Public.

Paso 6 – Validar la red Docker

Revisa que la red npm_default (o la que corresponda) no tenga la opción --internal. Si la tiene, elimínala y recrea la red:

docker network rm npm_default
docker network create npm_default
docker compose up -d

Una red sin aislamiento permite que el contenedor alcance cualquier IP del host.

Cuándo aplicar esta solución

  • Síntomas: 502 Bad Gateway en NPM, logs con connect() failed (111: Connection refused), acceso directo al servicio funciona desde el host y otros dispositivos LAN, pero no desde el contenedor.
  • Entorno: Docker Desktop en Windows con WSL2, servicio nativo (no contenedorizado) que escucha en puerto TCP.
  • Exclusiones: Si el servicio ya está escuchando en 0.0.0.0 y la regla de firewall está correcta, el problema suele radicar en la configuración del proxy (URL incorrecta, certificado expirado, etc.). En esos casos la solución anterior no es necesaria.

Código

# 1. Verificar conectividad desde el contenedor
docker exec -it npm bash -c "curl -v http://host.docker.internal:8096"

# 2. Añadir ruta estática en WSL2 (ejecutar dentro de la VM)
sudo ip route add 192.168.0.0/16 via $(ip route | grep default | awk '{print $3}')

# 3. Añadir host estático en docker‑compose
# (edita docker‑compose.yml)
extra_hosts:
  - "windows-host:host.docker.internal"

# 4. Regla de firewall en Windows
powershell -Command "New-NetFirewallRule -DisplayName 'Docker to Jellyfin' -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8096 -RemoteAddress 172.28.0.0/16"

# 5. Recrear red Docker sin aislamiento
docker network rm npm_default
docker network create npm_default
docker compose up -d

Verificación

  1. Desde el host abre http://localhost:8096 y confirma que Jellyfin responde.
  2. Desde el contenedor ejecuta curl -I http://windows-host:8096. Debes recibir HTTP/1.1 200 OK.
  3. Accede a la URL pública configurada en DuckDNS (https://mi-dominio.duckdns.org). NPM debe redirigir sin mostrar 502.
  4. Revisa los logs de NPM (docker logs npm) y busca la ausencia de mensajes connect() failed.
  5. Usa docker network inspect npm_default para confirmar que la subred incluye 172.28.0.0/16 y que no hay la opción "Internal": true.

Notas adicionales

  • En WSL2 la IP de la VM cambia tras cada reinicio. Si usas host.docker.internal no tendrás que actualizar nada; sin embargo, si optas por la IP estática de la LAN, verifica que siga siendo la misma después de cambios de red.
  • Algunas aplicaciones (por ejemplo, Sonarr, Radarr) requieren encabezados Host específicos. En NPM, habilita la opción Preserve Host Header cuando el backend verifica el hostname.
  • Si el contenedor sigue sin alcanzar el host, prueba con network_mode: host en Docker Desktop (solo funciona en Linux, no en Windows). En Windows la alternativa más fiable es extra_hosts + regla de firewall.
  • Mantén Docker Desktop actualizado; versiones anteriores tenían un bug que rompía host.docker.internal después de una suspensión del equipo.

Con estos pasos, la mayoría de los errores 502 derivados de la falta de conectividad entre Nginx Proxy Manager y servicios locales en Windows desaparecen, dejando la arquitectura de reverse proxy lista para producción o para un homelab personal.