Docker con VPN: puertos desaparecidos con Gluetun
Al usar network_mode: service:gluetun, los puertos publicados desaparecen. Entienda el espacio de nombres compartido y vea un compose funcional con Gluetun v3.41.3.
Por qué desaparecen los puertos al enrutar contenedores Docker mediante una VPN
Para enrutar contenedores Docker mediante 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 con network_mode: "service:gluetun". Esa conexión es lo 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 de la VPN. Los demás contenedores accederán a la aplicación mediante el nombre del contenedor de la VPN.
Si deja un bloque ports: en el contenedor conectado, Docker se niega a crearlo:
Error response from daemon: conflicting options: port publishing and the container type network modeLa herramienta utilizada aquí es Gluetun, un contenedor que conecta con 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 agosto de 2026. Los ejemplos usan 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 lo integra en una interfaz web.
Qué hace realmente network_mode: "service:gluetun"
Normalmente, cada contenedor 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 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_ADMINy/dev/net/tunporque crea la interfaz del túnel. El contenedor conectado no las hereda. - Compose rechaza cualquier archivo en el que un servicio defina
network_modeynetworksa la vez. 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 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-stoppedLa 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 desarrollo en curso. Por tanto, 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 sin cambiar 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 una dirección específica publica el puerto en todas las interfaces y crea su propia regla de firewall. Así es como los puertos publicados por Docker atraviesan directamente ufw.
Inícielo y compruébelo en este orden:
docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30docker 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 que se describe a continuación funcionará.
Mantenga las claves fuera del archivo de compose
gluetun.env contiene las credenciales y no se incluye en git:
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32Ambos valores proceden de un archivo de configuración de WireGuard que se genera en el área de cuenta del proveedor. Establezca el archivo con el modo 600. Sea claro sobre lo que esto aporta: la clave no se incluye en el 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 describe 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 de Docker compartida. Debe ser la red de gluetun, porque el contenedor conectado no tiene una red propia. Cómo se configuran 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 llega a la interfaz web de qBittorrent mediante gluetun:8080. No se necesita ninguna entrada ports:, porque el tráfico entre contenedores permanece en la red de 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. Fije esa versión o una posterior si un nombre no se resuelve.
El firewall de Gluetun decide quién puede abrir una conexión con él. Se permite el tráfico procedente de la propia red de Docker de gluetun. Un cliente de otra subred, un portátil de la LAN o un contenedor de una red bridge independiente se descarta hasta que indique esa subred:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24El 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 sirve para esas conexiones. Necesita un puerto reenviado por el proveedor y debe incluirlo en FIREWALL_VPN_INPUT_PORTS. Esta opción permite los puertos procedentes del lado del servidor VPN. Esta es la parte que la mayoría de las pilas multimedia creadas con Docker Compose deja sin configurar correctamente.
El interruptor de seguridad: qué ocurre cuando el túnel se cae
Este patrón justifica su complejidad cuando se produce un fallo. El contenedor conectado no tiene una segunda ruta. Su única salida de la máquina es el namespace que comparte, por lo que, cuando el túnel está caído, no hay ninguna ruta alternativa. El firewall de Gluetun aplica la misma regla desde el otro lado: el tráfico saliente pasa por el túnel o se dirige al endpoint del servidor VPN; todo lo demás se descarta. No existe un intervalo en el que los paquetes puedan salir 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, cuyo valor predeterminado es 1.1.1.1,8.8.8.8. 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. Si estas 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 timeoutLea 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 las causas. La documentación de Gluetun lo indica de forma explícita, porque muchas personas informan de la consecuencia y buscan la causa durante horas.
HEALTH_RESTART_VPN=on es el valor predeterminado y debe mantenerse activado. Desactívelo sólo mientras depura un fallo concreto, porque, si está desactivado, un túnel caído seguirá caído.
Orden de inicio: impedir que la pila se inicie antes de que el túnel esté activo
La imagen incluye un healthcheck de Docker:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckEse comando ejecuta una segunda copia temporal de gluetun, que consulta el servidor de estado de la instancia en ejecución en http://127.0.0.1:9999/. Un túnel operativo responde con 200 OK. Uno que no funciona responde con 500 Internal server error y una cadena de error, y el contenedor se marca como no saludable tras un único fallo.
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 con la red inactiva y a menudo abandona su primer intento de conexión. Healthchecks en Docker explica la sintaxis y los campos de temporización.
Hay un límite que suele causar problemas. Compose evalúa esa condición una sola vez, cuando crea el contenedor. No detiene ni reinicia la aplicación más adelante si gluetun pasa a estar no saludable. El mecanismo interno de auto-recuperación 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
El DNS (sistema de nombres de dominio) es la fuga que persiste aunque el túnel funcione 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 se cifren y circulen por el túnel.
La opción que rompe este comportamiento es DNS_UPSTREAM_PLAIN_ADDRESSES. Se suele activar cuando un nombre no se resuelve y se quiere que responda el resolvedor del router o del proveedor. La documentación de Gluetun indica claramente la consecuencia: todo el tráfico DNS no pasará por el túnel VPN y se filtrará fuera de él. El tráfico seguirá siendo privado. La lista de nombres de host no. La misma configuración incorrecta en WireGuard se explica en DNS que deja de resolverse a través de un túnel WireGuard.
Para probarlo, configure HTTPPROXY=on en gluetun y publique 8888:8888/tcp. Después, dirija un navegador a ese proxy y cargue una prueba de fugas de DNS. El resultado debería mostrar su proveedor o Cloudflare, pero nunca el router doméstico. La documentación de Gluetun advierte que algunas pruebas de fugas pueden mostrar resultados extraños, porque el resolvedor dentro del espacio de nombres es un intermediario de caché local y no el servidor que responde finalmente. Considere como señal real que aparezca un país incorrecto o el resolvedor de su propio ISP.
Añadir Tailscale junto al contenedor sidecar de la VPN y cuál tiene prioridad
Tailscale es una red superpuesta basada en WireGuard para acceder a sus propios equipos. Algunas personas lo ejecutan junto a una VPN de proveedor para mantener una vía de administración hacia el conjunto de servicios. Normalmente no entran en conflicto, por un motivo que conviene entender. La documentación de Tailscale establece el comportamiento predeterminado: funciona como una red superpuesta, sólo enruta tráfico entre dispositivos que ejecutan Tailscale y no afecta al 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 lo transporta por completo. Tailscale accede a la aplicación mediante
gluetun:8080, igual que cualquier otro contenedor externo. - Tailscale conectado al espacio de nombres de gluetun mediante
network_mode: "service:gluetun": necesita su propiocap_adddenet_adminynet_raw, porque las capacidades no se comparten automáticamente con el espacio de nombres. En el modo predeterminado de red en espacio de usuario,TS_USERSPACEestá activado; tailscaled no crea ninguna interfaz y funciona como proxy SOCKS5 o HTTP, por lo que no puede modificar el enrutamiento. Gluetun sigue transportando todo el tráfico. - Lo mismo, con
TS_USERSPACE=false: tailscaled crea un dispositivo de túnel e instala rutas, pero sólo para el rango de tailnet100.64.0.0/10y para las rutas de subred que anuncie medianteTS_ROUTES. El tráfico público sigue saliendo a través de gluetun. - Cualquiera de las opciones anteriores con un nodo de salida seleccionado,
sudo tailscale set --exit-node=<exit-node-ip>: Tailscale reclama la ruta predeterminada y tiene prioridad. No lo combine con gluetun. Sólo puede haber una ruta predeterminada y un único responsable.
Un efecto secundario resulta visible cuando Tailscale se ejecuta dentro del túnel. Sus pares ven la dirección del proveedor de VPN, por lo que es más probable que use relays. 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 necesitaba era 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 no puede 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. Configure 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 enlazar el mismo puerto. El proceso que pierde informa 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 todos los contenedores conectados 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. Reduzca WIREGUARD_MTU, pruebe 1400 y después 1320.
Gluetun nunca alcanza el estado saludable. La comprobación de arranque 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 y 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 que posee el espacio de nombres. Mueva la lista ports: al servicio gluetun. Si la dejó en el servicio asociado, 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 asociado no está conectado a 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, es descartado por el firewall de gluetun hasta que añada esa subred a FIREWALL_OUTBOUND_SUBNETS.
¿Funciona Gluetun como interruptor de corte cuando se interrumpe la VPN?
Sí, por dos motivos a la vez. El contenedor asociado no tiene otra ruta que la del espacio de nombres compartido, por lo que un túnel inactivo lo deja sin ninguna ruta fuera de la máquina. El firewall de Gluetun también permite el tráfico saliente sólo a través del túnel y hacia el endpoint del servidor VPN. Gluetun reinicia la VPN internamente y registra WARN [vpn] restarting VPN because it failed to pass the healthcheck, en lugar de salir, porque todos los contenedores asociados pierden la red cuando el propio gluetun se reinicia.
Tailscale y Gluetun en la misma pila: ¿cuál transporta el tráfico saliente?
Gluetun, excepto en una configuración. Tailscale sólo enruta de forma predeterminada el tráfico entre dispositivos de su tailnet y deja intacto el tráfico público. En el modo userspace predeterminado de la imagen del contenedor, no crea ninguna interfaz, por lo que no puede modificar el enrutamiento. Con TS_USERSPACE=false instala rutas sólo para 100.64.0.0/10 y las subredes anunciadas. La excepción es un nodo de salida: sudo tailscale set --exit-node=<exit-node-ip> convierte Tailscale en la ruta predeterminada, por lo que se impone. Elija un solo producto para gestionar la ruta predeterminada en lugar de superponer ambos.