Problema

En entornos Docker Swarm con un reverse proxy (por ejemplo, Traefik) y un proveedor de identidad (Authelia, Keycloak, etc.) es frecuente que los servicios que implementan OAuth2 necesiten redirigir al usuario a una URL de callback que apunta al host del clúster (ej. auth.example.com).

Los contenedores que forman parte de una overlay network de Swarm no pueden alcanzar directamente la IP del nodo host ni el nombre DNS que resuelve a esa IP. La resolución DNS funciona, pero la petición nunca llega porque Docker aísla la red del contenedor del stack de red del host. El síntoma típico es:

  • El navegador muestra la URL de callback (https://auth.example.com/...) y, al ser redirigida, el contenedor que ejecuta la aplicación (HedgeDoc, Immich, etc.) intenta contactar auth.example.com pero recibe “connection refused” o “timeout”.
  • Añadir una entrada extra_hosts con la IP del nodo funciona momentáneamente, pero la IP cambia cada vez que el Swarm se reinicia, obligando a actualizar manualmente el docker‑compose.yml.

Este patrón se repite en cualquier stack donde:

  1. El flujo OAuth2 depende de un endpoint externo que está detrás del mismo reverse proxy.
  2. Los contenedores están en una overlay network y no usan host network mode.
  3. No se dispone de un DNS interno que apunte a la IP del proxy dentro del clúster.

Causa

Docker Swarm crea redes overlay que son totalmente independientes de la red del host. Cada servicio recibe una IP virtual dentro del rango de la overlay y el tráfico entre contenedores se enruta a través del plano de datos de Swarm. Las razones principales por las que el contenedor no llega al host son:

  1. Aislamiento de red: por defecto, los contenedores no pueden usar la interfaz de loopback del nodo ni la IP del nodo. La tabla de rutas del contenedor no contiene una ruta hacia la subred del host.
  2. IP dinámica del nodo: cuando el nodo se reinicia, Docker asigna una nueva IP al ingress network (el que expone los puertos publicados). Las entradas estáticas en extra_hosts quedan obsoletas.
  3. Falta de descubrimiento interno: el nombre auth.example.com se resuelve mediante DNS externo a la IP del nodo, pero el contenedor no tiene una ruta de salida que apunte a esa IP, por lo que el paquete se descarta antes de salir del namespace de red.
  4. Política de seguridad: Docker impide que los contenedores accedan a la red del host para evitar que una aplicación comprometida pueda atacar al daemon o a otros procesos del host.

Solución

La solución consiste en exponer el endpoint de OAuth2 dentro de la propia overlay network, de modo que los contenedores lo consuman mediante un nombre de servicio Docker en lugar de la IP del host. Hay tres enfoques probados y reutilizables:

1. Publicar el proxy como servicio interno y usar su nombre DNS

  1. Declara Traefik (o cualquier otro reverse proxy) como un servicio Docker Swarm con modo host o ingress y exponlo en la overlay mediante la etiqueta traefik.enable=true.
  2. Añade una regla de router que escuche en un sub‑dominio interno, por ejemplo auth.internal.local.
  3. En los contenedores que necesiten el callback, configura la URL de redirección a https://auth.internal.local/callback.
  4. Docker DNS resolverá automáticamente auth.internal.local al IP virtual del servicio Traefik dentro de la overlay, sin depender de la IP del nodo.

Este método mantiene todo el tráfico dentro del clúster y evita la necesidad de extra_hosts.

2. Utilizar “network‑mode: host” solo para el servicio de autenticación

Si el proveedor de identidad no necesita aislamiento (por ejemplo, Authelia funciona exclusivamente como backend HTTPS), puedes lanzar el contenedor con:

services:
  authelia:
    image: authelia/authelia
    network_mode: host
    ports:
      - "9091:9091"

Al estar en host network mode, el contenedor comparte la pila de red del nodo y responde directamente a la IP del host. Los demás servicios siguen en la overlay y pueden referenciar auth.example.com sin problemas porque la petición sale del contenedor a la red del host y vuelve a entrar por el proxy.

3. Crear una red “attachable” y usar “extra_hosts” dinámico con Docker‑Compose

Cuando la primera opción no es viable (por ejemplo, se necesita un proxy externo distinto), se puede automatizar la generación de extra_hosts mediante un pequeño script que:

  1. Obtiene la IP actual del nodo para la red ingress:
    docker network inspect ingress -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}'
    
  2. Genera un archivo extra_hosts.yml con la entrada:
    extra_hosts:
      - "auth.example.com:<IP_GATEWAY>"
    
  3. Incluye ese archivo en el docker-compose.yml con !include extra_hosts.yml.

Al ejecutar docker stack deploy después de cada reinicio del nodo, el script actualiza la IP y el despliegue queda coherente sin intervención manual.

4. Usar un DNS interno (CoreDNS, Consul) que apunte al proxy

Instala un servidor DNS ligero dentro del Swarm (CoreDNS) y crea una zona que resuelva auth.example.com a la IP del servicio Traefik. Configura los contenedores para que usen ese DNS (opción --dns en la definición del servicio). De esta forma, cualquier cambio de IP del nodo se refleja automáticamente en la zona DNS sin tocar los manifiestos.

Cuándo aplicar esta solución

Señal / Síntoma Enfoque recomendado
Necesitas que todos los servicios usen la misma URL de callback y la URL está bajo tu dominio público. Opción 1 (proxy interno con sub‑dominio)
El proveedor de identidad no soporta TLS interno y depende del proxy para HTTPS. Opción 1 o Opción 3 (automatizar extra_hosts)
El contenedor de autenticación es el único que necesita acceso directo al host y no hay conflicto de puertos. Opción 2 (host network mode)
Prefieres mantener la arquitectura “cero cambios en la aplicación” y ya dispones de un DNS interno. Opción 4 (CoreDNS/Consul)
No puedes modificar la configuración del reverse proxy (p. ej. SaaS) Opción 3 (script de extra_hosts)

No apliques la solución si:

  • Tu política de seguridad prohíbe host network mode.
  • El proxy externo está fuera del control del clúster y no puedes crear un sub‑dominio interno.
  • No deseas introducir un DNS adicional por complejidad operativa.

Código

# 1. Crear una overlay network attachable (si aún no existe)
docker network create \
  --driver overlay \
  --attachable \
  swarm_net

# 2. Deploy de Traefik como servicio interno con nombre DNS "traefik"
docker service create \
  --name traefik \
  --network swarm_net \
  --publish 80:80 \
  --publish 443:443 \
  --label traefik.enable=true \
  --label traefik.http.routers.auth.rule="Host(`auth.internal.local`)" \
  --label traefik.http.services.auth.loadbalancer.server.port=9091 \
  traefik:v2.10

# 3. Deploy de Authelia (puede usar host mode o overlay)
docker service create \
  --name authelia \
  --network swarm_net \
  --publish 9091:9091 \
  --label traefik.enable=true \
  --label traefik.http.routers.authelia.rule="Host(`auth.internal.local`)" \
  --label traefik.http.routers.authelia.entrypoints=websecure \
  authelia/authelia:latest

En los servicios que consumen OAuth2 (HedgeDoc, Immich, etc.) basta con apuntar la URL de callback a https://auth.internal.local/callback. Docker DNS resolverá auth.internal.local al IP virtual de Traefik dentro de swarm_net.


## Verificación
1. **Comprobar resolución DNS interna**  
   ```bash
   docker run --rm --network swarm_net alpine nslookup auth.internal.local

Debería devolver una IP del rango de la overlay (ej. 10.0.0.5).

  1. Probar la ruta de callback
    Desde cualquier contenedor del stack, ejecuta:

    curl -I https://auth.internal.local/api/verify
    

    Debería responder 200 OK sin timeout.

  2. Reiniciar un nodo
    Detén y enciende el nodo del manager, luego vuelve a lanzar docker service ls. Verifica que la IP de auth.internal.local sigue resolviéndose y que la aplicación OAuth2 completa el flujo sin intervención manual.

  3. Revisar logs de Traefik

    docker service logs traefik --tail 20
    

    Busca entradas que confirmen que la petición a /callback llegó y fue redirigida correctamente.

Notas adicionales

  • Mantén la overlay lo más pequeña posible: demasiadas sub‑redes pueden generar colisiones de IP y dificultar la depuración.
  • TLS terminación: si usas Traefik para terminar TLS, asegúrate de que el backend (Authelia) acepte tráfico HTTP interno; de lo contrario, habilita traefik.http.services.auth.loadbalancer.server.scheme=https.
  • Persistencia de la configuración de DNS interno: cuando uses CoreDNS o Consul, exporta la zona como ConfigMap o archivo de configuración versionado para que los despliegues sean reproducibles.
  • Monitoreo: agrega métricas de Traefik (/metrics) y de Authelia para detectar rápidamente fallos de callback después de cambios de infraestructura.