SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor

Cómo enrutar contenedores Docker a través de una VPN

Al usar network_mode: service:gluetun, los puertos desaparecen porque el contenedor pierde su stack de red. Aprende a publicar puertos en el contenedor VPN sin errores.

Por qué desaparecen los puertos al enrutar contenedores Docker a través de una VPN

Para enrutar contenedores Docker a través de una VPN, se asigna el túnel a un contenedor y después se conectan los demás a su espacio de nombres de red mediante network_mode: "service:gluetun". Esta conexión es la parte que suele causar confusión. El contenedor conectado deja de tener una red propia, por lo que también desaparecen sus puertos publicados y su nombre de servicio de Docker. Publique los puertos en el contenedor VPN. Los demás contenedores accederán a la aplicación mediante el nombre del contenedor VPN.

Si deja un bloque ports: en el contenedor conectado, Docker se negará a crearlo:

Error response from daemon: conflicting options: port publishing and the container type network mode

La herramienta utilizada aquí es Gluetun, un contenedor que se conecta a un proveedor comercial de VPN (red privada virtual) mediante WireGuard u OpenVPN y aplica su propio firewall. La versión v3.41.3 es la actual en octubre de 2026. Los ejemplos utilizan Mullvad con WireGuard, por lo que necesita una cuenta y una clave de su proveedor. Si prefiere terminar el túnel en hardware propio, ejecutar su propio servidor WireGuard en un VPS crea el otro extremo, y wg-easy en Docker proporciona una interfaz web para administrarlo.

Qué hace realmente network_mode: "service:gluetun"

Normalmente, cada contenedor de Docker obtiene su propio espacio de nombres de red: sus propias interfaces, tabla de enrutamiento, reglas de firewall y sockets en escucha. El modo service: omite ese paso e inicia el contenedor dentro del espacio de nombres de gluetun. Un solo espacio de nombres implica una sola dirección IP, y eso cambia seis aspectos.

  • La aplicación no tiene una dirección propia. Usa la dirección de gluetun.
  • La aplicación no está conectada a ninguna red de Docker, por lo que su nombre de servicio nunca se registra ni se resuelve. Los demás contenedores deben usar gluetun.
  • Los contenedores que están dentro del espacio de nombres se comunican entre sí mediante localhost.
  • Dos contenedores del mismo espacio de nombres no pueden escuchar en el mismo puerto. La documentación de Gluetun lo indica claramente: no existe ninguna solución alternativa.
  • Las capacidades pertenecen a un contenedor, no a un espacio de nombres. Gluetun tiene NET_ADMIN y /dev/net/tun porque crea la interfaz del túnel. El contenedor conectado no las hereda.
  • Compose rechaza cualquier archivo en el que un servicio establezca a la vez network_mode y networks. Conecte gluetun a sus redes y la aplicación utilizará esas redes.

Reiniciar gluetun desconecta todo lo que está conectado a él. Este comportamiento está documentado y es la razón por la que gluetun reinicia el proceso VPN dentro del contenedor en lugar de salir cuando falla la conexión. Después de reiniciar o recrear gluetun manualmente, reinicie los contenedores conectados a él.

El archivo de Compose que funciona

services:
  gluetun:
    image: qmcgaw/gluetun:v3
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    environment:
      - VPN_SERVICE_PROVIDER=mullvad
      - VPN_TYPE=wireguard
      - SERVER_CITIES=Amsterdam
      - TZ=Europe/Amsterdam
    env_file:
      - ./gluetun.env
    volumes:
      - ./gluetun:/gluetun
    ports:
      - 127.0.0.1:8080:8080/tcp
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Amsterdam
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent:/config
      - ./downloads:/downloads
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

La etiqueta :v3 es la versión estable más reciente de la serie v3. La etiqueta :latest apunta al último commit de la rama master, que corresponde al estado de desarrollo. Por eso, fije :v3 en una máquina que no quiera depurar un martes.

WEBUI_PORT=8080 debe coincidir con el puerto publicado, porque qBittorrent se enlaza dentro del namespace de gluetun y la regla de publicación envía el tráfico del host al puerto 8080 en ese namespace. Si cambia un número y no el otro, el puerto no responderá. 127.0.0.1:8080:8080 mantiene la interfaz web en la dirección de loopback del host. Un 8080:8080 sin dirección publica el puerto en todas las interfaces y crea su propia regla de firewall. Así es como los puertos publicados por Docker pasan directamente por alto ufw.

Inícielo y compruébelo en este orden:

docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30

docker compose ps debería mostrar gluetun como healthy y qbittorrent como running. Después, confirme la dirección de salida desde dentro del namespace. Esta comprobación determina todo lo demás:

docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"

El campo ip de ese JSON debería contener la dirección de su proveedor de VPN. Si contiene la dirección de su propio servidor, la aplicación no está dentro del túnel y nada de lo siguiente funcionará como se describe.

Mantenga las claves fuera del archivo compose

gluetun.env contiene las credenciales y se mantiene fuera de git:

WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32

Ambos valores proceden de un archivo de configuración de WireGuard que genera en el área de cuenta de su proveedor. Establezca el archivo con el modo 600. Sea preciso sobre lo que consigue: la clave queda fuera del repositorio, pero docker inspect gluetun sigue mostrando todas las variables de entorno a cualquiera que pueda acceder al socket de Docker. Archivos de entorno y secretos en Docker Compose explica opciones más seguras.

Cómo se comunica un contenedor externo al túnel con uno interno

La comunicación funciona en ambas direcciones y cada una usa un nombre diferente. Los dos contenedores necesitan una red Docker compartida. En este caso, debe ser la red de gluetun, porque el contenedor conectado no tiene una red propia. Cómo se conectan las redes de Docker Compose explica los valores predeterminados.

Para comunicarse desde fuera hacia dentro, use el nombre de gluetun y el puerto en el que escucha la aplicación. Un contenedor de reverse proxy accede a la interfaz web de qBittorrent en gluetun:8080. No se necesita ninguna entrada ports:, porque el tráfico entre contenedores permanece en la red Docker y nunca pasa por un puerto del host.

Para comunicarse desde dentro hacia fuera, use el nombre del servicio del otro contenedor, por ejemplo, postgres:5432. Gluetun resuelve los nombres de otros contenedores desde su propio espacio de nombres desde v3.41. Si un nombre no se resuelve, fije esa versión o una posterior.

El firewall de Gluetun decide quién puede abrir una conexión hacia él. Se permite el tráfico procedente de la propia red Docker de gluetun. Un cliente de otra subred, un portátil de la LAN o un contenedor de una red bridge independiente necesita una ruta de retorno permitida por el firewall y el enrutamiento de gluetun:

FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24

El significado documentado es exacto: subredes separadas por comas a las que Gluetun y los contenedores que comparten su pila de red pueden acceder.

Las conexiones entrantes desde Internet son un problema independiente. Los peers de un cliente torrent llegan desde el lado de la VPN, por lo que publicar el puerto 6881 en el host no les sirve. Necesita un puerto redirigido por su proveedor y debe incluirlo en FIREWALL_VPN_INPUT_PORTS, que permite puertos desde el lado del servidor VPN. Mullvad, que se usa en este ejemplo, no admite el reenvío de puertos. Por tanto, necesita un proveedor que sí lo admita. Esta es la parte que la mayoría de las pilas multimedia creadas con Docker Compose deja sin resolver.

El interruptor de emergencia: qué ocurre cuando el túnel se cae

Este patrón justifica su complejidad cuando se produce un fallo. El contenedor conectado comparte las interfaces y la tabla de enrutamiento de Gluetun. El firewall de Gluetun bloquea el tráfico normal de Internet fuera del túnel, pero permite el endpoint del servidor VPN, el tráfico local de Docker y las excepciones configuradas. Ese firewall impide que el tráfico vuelva a salir por la interfaz normal cuando el túnel se cae. No existe un intervalo en el que los paquetes se filtren por la interfaz normal mientras el cliente se vuelve a conectar.

Gluetun supervisa su propia conexión. Cada minuto envía un eco ICMP (un ping) a las direcciones de HEALTH_ICMP_TARGET_IPS, que de forma predeterminada son 1.1.1.1,8.8.8.8. Si ICMP no está permitido, esa comprobación pequeña usa en su lugar una consulta DNS UDP sin cifrar. Cada cinco minutos establece una conexión TCP y TLS (seguridad de la capa de transporte) completa con HEALTH_TARGET_ADDRESSES, cuyo valor predeterminado es cloudflare.com:443,github.com:443. Cuando esas comprobaciones fallan, reinicia la VPN dentro del contenedor y lo registra:

WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout

Revise los registros del contenedor conectado teniendo en cuenta ese orden. Las líneas como connection refused, operation not permitted y i/o timeout dentro de la aplicación son consecuencias de un túnel caído, no sus causas. La documentación de Gluetun lo indica explícitamente, porque los usuarios informan de la consecuencia y la investigan durante horas.

HEALTH_RESTART_VPN=on está habilitado de forma predeterminada y debe mantenerse así. Desactívelo sólo mientras depura un fallo concreto, porque, si está desactivado, un túnel caído permanece caído.

Orden de inicio: evitar que la pila se inicie antes de que el túnel esté activo

La imagen incluye una comprobación de estado de Docker:

HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck

Ese comando inicia una segunda copia de gluetun, de corta duración, que consulta el servidor de estado de la instancia en ejecución en http://127.0.0.1:9999/. Un túnel operativo responde 200 OK. Uno que no funciona responde 500 Internal server error con una cadena de error, y Docker marca el contenedor como no saludable después de tres comprobaciones fallidas consecutivas.

condition: service_healthy es lo que espera a que eso ocurra. El valor simple depends_on: [gluetun] sólo espera a que se inicie el contenedor. Esto sucede varios segundos antes de que se complete el handshake, por lo que la aplicación se inicia sin red operativa y a menudo abandona su primer intento de conexión. Comprobaciones de estado en Docker Compose explica la sintaxis y los campos de temporización.

Hay una limitación que suele causar problemas. Compose evalúa esa condición cuando inicia la pila. No detiene ni reinicia la aplicación más adelante si gluetun pasa a estar no saludable. La recuperación automática interna de gluetun cubre ese caso. Por eso reinicia el proceso VPN en lugar del contenedor.

Compruebe si hay una fuga de DNS antes de confiar en la configuración

DNS (sistema de nombres de dominio) es la fuga que persiste aunque el túnel esté configurado correctamente. Gluetun ejecuta su propio resolvedor dentro del espacio de nombres y reenvía las consultas mediante DoT (DNS sobre TLS) a Cloudflare de forma predeterminada: DNS_UPSTREAM_RESOLVER_TYPE=dot y DNS_UPSTREAM_RESOLVERS=cloudflare. No modifique ninguno de los dos valores para que las consultas estén cifradas y viajen por el túnel.

La configuración que provoca este problema cuando apunta a un resolvedor local es DNS_UPSTREAM_PLAIN_ADDRESSES. Se suele usar cuando un nombre no se resuelve y se quiere que responda el router o el resolvedor del proveedor. La documentación de Gluetun advierte que las consultas enviadas a un resolvedor en el puente de Docker o en la LAN salen fuera del túnel VPN. Un resolvedor público sin cifrado sigue recibiendo las consultas a través del túnel, pero el proveedor de VPN puede leerlas y modificarlas. La misma equivocación en WireGuard se trata en DNS que deja de resolverse a través de un túnel WireGuard.

Para probarlo, establezca SHADOWSOCKS=on y SHADOWSOCKS_PASSWORD en gluetun, publique 8388:8388/tcp y 8388:8388/udp, conéctese con un cliente Shadowsocks configurado para tunelizar el DNS mediante UDP y cargue una prueba de fugas de DNS. El resultado debe identificar a su proveedor o a Cloudflare, nunca al router doméstico. La documentación de Gluetun advierte que algunas pruebas de fugas muestran resultados inusuales porque el resolvedor dentro del espacio de nombres es un intermediario local de caché, no el servidor que finalmente responde. Considere como señal real que aparezca un país incorrecto o el resolvedor de su propio ISP.

Añadir Tailscale junto al contenedor auxiliar de la VPN y determinar cuál prevalece

Tailscale es una red superpuesta basada en WireGuard para acceder a sus propias máquinas. Muchas personas la ejecutan junto a una VPN de proveedor para mantener una vía de administración hacia el conjunto de servicios. Compartir un espacio de nombres también implica compartir las reglas del firewall, por lo que ambas configuraciones de firewall y de enrutamiento deben ser compatibles. La documentación de Tailscale establece el comportamiento predeterminado: actúa como una red superpuesta, sólo enruta tráfico entre dispositivos que ejecutan Tailscale y no modifica el tráfico público de Internet.

Por tanto, la respuesta depende de un ajuste.

  • Tailscale en su propio contenedor, con la configuración predeterminada: nunca ve el tráfico saliente de la aplicación. Gluetun transporta todo ese tráfico. Un proceso del contenedor de Tailscale accede a la aplicación en gluetun:8080 mediante una red Docker compartida. Para exponer esa conexión a los clientes de tailnet se necesita reenvío o un proxy.
  • Tailscale conectado al espacio de nombres de gluetun con network_mode: "service:gluetun": en el modo predeterminado de red en espacio de usuario, TS_USERSPACE está activado, tailscaled no crea ninguna interfaz y funciona como proxy SOCKS5 o HTTP, por lo que no puede modificar el enrutamiento. Gluetun sigue transportando el tráfico saliente normal, salvo que una aplicación use explícitamente ese proxy.
  • Lo mismo, con TS_USERSPACE=false: Tailscale necesita su propio dispositivo /dev/net/tun y sus capacidades, porque no se heredan del espacio de nombres. Tailscaled crea un dispositivo de túnel y, en Linux, normalmente instala rutas /32 IPv4 individuales hacia los pares, en lugar de una única ruta para 100.64.0.0/10. Definir sólo TS_USERSPACE=false no habilita la ruta agregada. Sólo instala las rutas de subred de otros nodos si se pasa --accept-routes mediante TS_EXTRA_ARGS. TS_ROUTES funciona en la dirección contraria: anuncia al resto de su tailnet las subredes a las que este contenedor puede acceder, el equivalente de tailscale set --advertise-routes=, y no añade ninguna ruta aquí. El tráfico público sigue saliendo a través de gluetun.
  • Red en modo kernel en el espacio de nombres de gluetun con un nodo de salida seleccionado, sudo tailscale set --exit-node=<exit-node-ip>: Tailscale instala rutas predeterminadas en ese espacio de nombres, pero el firewall de Gluetun puede bloquear esa ruta. Seleccionar un nodo de salida en un contenedor separado no modifica las rutas de la aplicación, y el modo de espacio de usuario sólo transporta el tráfico que se envía explícitamente mediante su proxy. No combine rutas predeterminadas en conflicto en el espacio de nombres de gluetun. Una ruta predeterminada, un solo responsable.

Si esas rutas anunciadas son el objetivo y quiere que se pueda acceder a toda la red privada situada detrás del equipo, en lugar de sólo al equipo, ejecutar un router de subred de Tailscale en una VPS explica la aprobación de rutas, el reenvío IP y la opción del cliente que TS_ROUTES no configura por sí sola.

Si Tailscale se utiliza para proporcionarle una URL de administración y no una ruta, tailscale serve y tailscale funnel colocan HTTPS delante de un servicio HTTP local. Sólo funnel lo expone a Internet público. En Tailscale v1.104.5, sus proxies inversos HTTP aceptan http://gluetun:8080 directamente cuando el contenedor de Tailscale puede acceder a él mediante una red Docker compartida. Incluya el esquema http:// para un destino que no sea localhost. También funciona compartir el espacio de nombres de red de gluetun y dirigir el tráfico a http://127.0.0.1:8080.

Se observa un efecto secundario cuando Tailscale se ejecuta dentro del túnel. Sus pares ven la dirección del proveedor de VPN, por lo que es normal que utilice relays con más frecuencia. tailscale status muestra relay "..." junto a un par en lugar de direct cuando ocurre esto. La conexión funciona, pero es más lenta. Si lo único que necesita realmente es la red superpuesta, la diferencia entre WireGuard sin más y Tailscale es un mejor punto de partida.

Qué falla y qué mensaje verá

Docker se niega a crear el contenedor de la aplicación. Error response from daemon: conflicting options: port publishing and the container type network mode significa que todavía hay un bloque ports: en el servicio conectado. Muévalo a gluetun.

Compose rechaza todo el archivo. Un servicio no puede definir network_mode y networks al mismo tiempo. Coloque las redes en gluetun.

Otro contenedor no puede resolver la aplicación. curl: (6) Could not resolve host: qbittorrent es el comportamiento esperado, porque el contenedor conectado no se unió a ninguna red ni registró ningún nombre. Use gluetun y el puerto.

El segundo contenedor conectado no se inicia. Dos procesos del mismo espacio de nombres no pueden asociar el mismo puerto. El proceso que pierde informa de que la dirección ya está en uso. Cambie el puerto interno de la aplicación o ejecute otro gluetun.

La aplicación se queda sin red después de modificar gluetun. Reiniciar o recrear gluetun interrumpe la conectividad de todo lo que está conectado a él. Reinicie esos contenedores.

Las páginas pequeñas cargan y las grandes se quedan bloqueadas. Es un problema de MTU (unidad máxima de transmisión). El túnel añade sobrecarga y algún elemento de la ruta descarta los paquetes demasiado grandes sin devolver un error. Compruebe WIREGUARD_MTU, cuyo valor predeterminado en v3.41.3 es 1320, y redúzcalo si la ruta requiere paquetes más pequeños. La imagen de desarrollo usa el descubrimiento de MTU de la ruta cuando este ajuste no está definido.

Gluetun nunca alcanza el estado saludable. La comprobación de inicio indica los primeros elementos que debe revisar: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. Compruebe si la clave ha caducado. Después, compruebe si la lista de servidores está obsoleta. Por último, compruebe si el firewall del host bloquea el tráfico UDP saliente.

FAQ

¿Por qué dejaron de funcionar los puertos publicados de mi contenedor detrás de Gluetun?

Porque network_mode: "service:gluetun" coloca el contenedor en el espacio de nombres de red de Gluetun, y un espacio de nombres tiene una dirección IP y un conjunto de puertos de escucha. La aplicación sigue escuchando, pero la regla de publicación debe estar en el contenedor propietario del espacio de nombres. Mueva la lista ports: al servicio de Gluetun. Si la dejó en el servicio conectado, Docker ni siquiera la creará: Error response from daemon: conflicting options: port publishing and the container type network mode.

¿Cómo accedo desde un contenedor externo a un contenedor dentro del túnel VPN?

Use el nombre del servicio de Gluetun y el puerto en el que escucha la aplicación, por ejemplo gluetun:8080. El contenedor conectado no tiene ninguna red Docker propia, por lo que su nombre nunca se resuelve. No es necesario publicar puertos para el tráfico entre contenedores. En sentido contrario, un contenedor dentro del espacio de nombres puede acceder a un contenedor externo mediante su nombre de servicio, como postgres:5432, en Gluetun v3.41 y versiones posteriores. Un cliente de otra subred, como un portátil de su LAN, necesita una ruta de retorno permitida por el firewall y el enrutamiento de Gluetun; añada esa subred a FIREWALL_OUTBOUND_SUBNETS.

¿Gluetun funciona como un interruptor de corte cuando se cae la VPN?

Sí, con el firewall habilitado. El contenedor conectado comparte las interfaces y las rutas de Gluetun, y el firewall bloquea el tráfico normal de Internet fuera del túnel, a la vez que permite el endpoint del servidor VPN, el tráfico local de Docker y las excepciones configuradas. Gluetun reinicia internamente la VPN y registra WARN [vpn] restarting VPN because it failed to pass the healthcheck, en lugar de salir, porque todos los contenedores conectados pierden la red cuando se reinicia el propio Gluetun.

Tailscale y Gluetun en el mismo stack: ¿cuál transporta el tráfico saliente?

Gluetun transporta el tráfico saliente normal, salvo que lo envíe explícitamente a través de Tailscale. De forma predeterminada, Tailscale sólo enruta el tráfico entre dispositivos de su tailnet y no modifica el tráfico público. En el modo userspace predeterminado de la imagen del contenedor, no crea ninguna interfaz, por lo que no puede afectar al enrutamiento. Con TS_USERSPACE=false, Linux normalmente obtiene rutas /32 IPv4 individuales hacia los pares, en lugar de una ruta agregada 100.64.0.0/10, además de las rutas de subred de otros nodos sólo si añade --accept-routes mediante TS_EXTRA_ARGS. TS_ROUTES anuncia las subredes de este contenedor a su tailnet y no añade rutas locales. Al seleccionar un exit node con sudo tailscale set --exit-node=<exit-node-ip>, sólo se instalan rutas predeterminadas en el espacio de nombres que usa la red de kernel de Tailscale; un contenedor independiente no modifica las rutas de la aplicación, y el modo userspace sólo transporta el tráfico enviado a través de su proxy. Elija un solo producto para gestionar la ruta predeterminada en lugar de apilar ambos.

¿Te resultó útil este artículo?