Gluetun: acceder al host y a otros contenedores
Un contenedor detrás de Gluetun no tiene interfaces propias: publica sus puertos en Gluetun y permite solo las subredes externas que necesita.
Qué ocurre cuando un contenedor se une a la red de gluetun
Un contenedor que establece network_mode: service:gluetun no tiene interfaces de red propias. Se une al espacio de nombres de red de gluetun, por lo que la publicación de puertos y las reglas del firewall dejan de ser propiedades de ese contenedor y pasan a ser propiedades del servicio gluetun. Todas las respuestas siguientes se derivan de ese hecho.
Un espacio de nombres de red es una copia privada de la pila de red del kernel: tiene sus propias interfaces, su propia tabla de enrutamiento, sus propias reglas del firewall y sus propios sockets en escucha. Docker asigna uno a cada contenedor de forma predeterminada. Cuando se escribe network_mode: service:gluetun, Docker omite ese paso y coloca el contenedor nuevo dentro del espacio de nombres que ya posee gluetun. El contenedor conserva su propio sistema de archivos y su propio archivo /etc/hosts, y este último será importante más adelante.
Puede verlo directamente.
docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrentEsto muestra container: seguido del ID del contenedor gluetun, mientras que un contenedor normal mostraría bridge. Esta guía continúa desde enrutar el tráfico de Docker a través de una VPN con gluetun: el túnel funciona y ahora nada puede comunicarse con el contenedor.
Publicar el puerto en gluetun, no en la aplicación
Si dejas un bloque ports: en el servicio que establece network_mode, Docker se niega a crear el contenedor:
Error response from daemon: conflicting options: port publishing and the container type network modeLa causa es directa. Publicar un puerto significa añadir una regla NAT (traducción de direcciones de red) que reenvía un puerto del host al espacio de nombres de red propio de un contenedor, y este contenedor no tiene uno. Mueve la asignación al servicio gluetun. El número de puerto no cambia porque la aplicación sigue escuchando en ese puerto dentro del espacio de nombres compartido.
services:
gluetun:
ports:
- "8080:8080/tcp" # qBittorrent web UI
qbittorrent:
network_mode: "service:gluetun"
# no ports: block hereUn bloque expose: en el servicio dependiente tampoco sirve de nada, y un bloque networks: en ese servicio impide continuar: Compose informa de que el servicio declara network_mode y networks, que son mutuamente excluyentes, y se niega a cargar el archivo.
Una consecuencia aparece más adelante. Todos los contenedores del espacio de nombres comparten un único espacio de puertos. Por tanto, dos aplicaciones que usan 8080 de forma predeterminada entran en conflicto, y la segunda que se inicia falla con un error que indica que la dirección ya está en uso. Cambia el valor de una de ellas en su propia configuración. Por ejemplo, cambia la variable WEBUI_PORT en la imagen LinuxServer qBittorrent y publica el nuevo número en gluetun.
¿Cómo se comunican entre sí los contenedores detrás de gluetun?
Dentro del espacio de nombres ya comparten una interfaz de loopback. Un contenedor detrás de gluetun accede a su contenedor hermano en 127.0.0.1:<port> sin usar ninguna red de Docker.
Desde fuera del espacio de nombres, el contenedor no tiene nombre. El DNS integrado de Docker resuelve un nombre de servicio en la dirección de ese servicio dentro de una red definida por el usuario, pero este contenedor no tiene una dirección en ninguna red. Por tanto, un contenedor normal como Sonarr no accede al cliente de torrent en http://qbittorrent:8080. Accede a él en http://gluetun:8080, porque el socket está escuchando en el espacio de nombres de gluetun, en la dirección de gluetun. Esto sorprende a quienes conocen cómo funcionan las redes y los nombres de servicio de Docker Compose y esperan que se aplique la resolución de nombres habitual. También funciona sin publicar nada en el host, porque ambos contenedores están en la misma red de Compose.
Compruebe el DNS antes de depurar cualquier otra cosa. Gluetun ejecuta su propio resolvedor y reescribe /etc/resolv.conf en su propio contenedor, pero /etc/resolv.conf es un archivo específico de cada contenedor, por lo que el archivo que escribió gluetun no es el que lee su aplicación.
docker exec qbittorrent cat /etc/resolv.conf¿Cómo accedo a un servicio que se ejecuta en el host de Docker?
Use host.docker.internal. Requiere dos ajustes en dos lugares distintos porque hay dos problemas diferentes.
Primero se configura el nombre. /etc/hosts es específico de cada contenedor, por lo que la entrada extra_hosts debe estar en el contenedor de la aplicación, no en gluetun.
prowlarr:
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"host-gateway es un valor especial que Docker sustituye por una dirección interna del propio host. En una instalación normal de Docker en Linux, es la dirección del puente docker0, normalmente 172.17.0.1. Confirme la suya con ip -4 addr show docker0 en el VPS. Docker Desktop resuelve este nombre por su cuenta. Por eso las guías escritas para un portátil omiten la línea extra_hosts y el mismo archivo falla después en un servidor.
Después se configura la ruta. Añadir el nombre sólo indica al contenedor qué dirección debe usar. El paquete sigue saliendo por la ruta predeterminada de gluetun, que es el túnel, y el firewall de gluetun lo descarta. El síntoma es una conexión que queda esperando y finalmente agota el tiempo de espera, no una conexión rechazada. Un rechazo significa que el paquete llegó y algo respondió negativamente. Un tiempo de espera agotado significa que nunca llegó.
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32Después, compruebe que el servicio del host realmente está escuchando en esa dirección. Un servidor PostgreSQL vinculado sólo a 127.0.0.1 no es accesible desde ningún contenedor, con túnel o sin él, porque 127.0.0.1 dentro del espacio de nombres es el loopback de ese propio espacio de nombres. Vincúlelo a 172.17.0.1: aceptará conexiones desde los contenedores sin exponerse en la interfaz pública. Verifíquelo con ss -lntp | grep 5432 en el host.
Qué cambia realmente FIREWALL_OUTBOUND_SUBNETS
La documentación de gluetun lo describe como las subredes separadas por comas a las que gluetun y los contenedores que comparten su pila de red pueden acceder, y señala que implica cambios en el firewall y en el enrutamiento. Ambas partes son importantes. Gluetun añade una ruta para cada subred indicada a través de la puerta de enlace del puente de Docker, por lo que los paquetes destinados a esas direcciones salen por eth0 en lugar de hacerlo por el túnel. También abre el firewall para esas subredes, porque gluetun descarta de lo contrario el tráfico saliente que no está dirigido al servidor VPN.
Escriba el valor sin espacios después de las comas.
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32Hay dos propiedades fáciles de pasar por alto. Esta configuración se aplica en el ámbito del espacio de nombres, por lo que afecta a todos los contenedores que están detrás de gluetun, no sólo al que tenía en mente. Además, sólo se aplica al tráfico saliente: controla las conexiones que inicia un contenedor. Las conexiones que llegan a un puerto publicado siguen una ruta diferente y no necesitan ninguna entrada aquí.
Acceso a la interfaz web desde un par de Tailscale
Tailscale asigna a cada máquina una dirección de 100.64.0.0/10, el rango reservado para NAT de nivel de operador. Nada de esto tiene coste en un tailnet personal, aunque los límites de usuarios y dispositivos del plan gratuito determinan si esto sigue siendo así cuando otras personas necesitan acceder a las mismas interfaces. Como la facturación cuenta los usuarios y no las máquinas, el coste real de un tailnet de pago depende de cuántas personas invite, no de cuántos contenedores les exponga. Cada dirección requiere un trabajo diferente.
La entrada es sencilla. Publicar 8080:8080 en gluetun enlaza ese puerto en todas las direcciones del host, incluida la interfaz tailscale0 del host. Por tanto, un par abre http://<machine-name>:8080 y llega al contenedor. Gluetun no interviene en esta ruta, porque la regla NAT de Docker está en el host, fuera del espacio de nombres.
Para que la interfaz sólo sea accesible a través del tailnet, enlace el puerto publicado a la dirección Tailscale del host en lugar de enlazarlo a todas las direcciones.
ports:
- "100.101.102.103:8080:8080/tcp"Obtenga esa dirección con tailscale ip -4 en el host. En este caso, enlazar el puerto ofrece un control más estricto que una regla de firewall, porque el puerto nunca se abre en la interfaz pública. Si prefiere acceder a la interfaz mediante un nombre HTTPS en lugar de mediante un host y un puerto, tailscale serve puede publicar ese puerto, aunque serve y funnel se diferencian en quién puede acceder finalmente y sólo uno de los dos mantiene la interfaz dentro del tailnet. Esto también evita el problema descrito en Publicar puertos de Docker directamente fuera de ufw.
La salida es donde vuelve a intervenir FIREWALL_OUTBOUND_SUBNETS. Si un contenedor debe conectarse a un par, añada la dirección de ese par y prefiera un /32 por par en lugar de usar todo /10. Si la máquina a la que se conecta está en una red privada accesible mediante un VPS que anuncia esa subred a su tailnet, indique el rango anunciado en lugar de la dirección 100.x del propio router y confirme que el host haya aceptado esas rutas. Los nombres de MagicDNS no se resolverán dentro del contenedor, porque el contenedor no usa el resolvedor del host. Use la dirección numérica 100.x o fíjela con una línea extra_hosts. Lo mismo se aplica cuando ejecuta su propio servidor de control de Tailscale con Headscale.
Un archivo compose completo para la estructura habitual
Un cliente de descargas detrás de la VPN, dos interfaces web que responden sólo en la tailnet y un contenedor que lee una base de datos PostgreSQL que se ejecuta en el host.
services:
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- "127.0.0.1:8000:8000/tcp" # gluetun control server, host only
- "100.101.102.103:8080:8080/tcp" # qBittorrent UI, tailnet only
- "100.101.102.103:9696:9696/tcp" # Prowlarr UI, tailnet only
volumes:
- ./gluetun:/gluetun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
- SERVER_CITIES=Amsterdam
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,100.64.7.9/32
- TZ=Europe/Amsterdam
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- /srv/downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- PROWLARR__POSTGRES__HOST=host.docker.internal
- PROWLARR__POSTGRES__PORT=5432
- PROWLARR__POSTGRES__USER=prowlarr
- PROWLARR__POSTGRES__PASSWORD=${PROWLARR_DB_PASSWORD}
- PROWLARR__POSTGRES__MAINDB=prowlarr-main
- PROWLARR__POSTGRES__LOGDB=prowlarr-log
volumes:
- ./prowlarr:/config
depends_on:
gluetun:
condition: service_healthy
restart: unless-stoppedLea el archivo para entender el patrón, no los nombres de los productos. Ambas interfaces se publican en gluetun y se vinculan a la dirección de la tailnet del host, por lo que responden en Tailscale y en ningún otro lugar. Sólo Prowlarr incluye la línea extra_hosts, porque Prowlarr es el contenedor que resuelve los nombres host.docker.internal. FIREWALL_OUTBOUND_SUBNETS define dos direcciones individuales: la dirección del puente Docker del host, para que Prowlarr pueda abrir una conexión con la base de datos, y un equipo de la tailnet.
El servidor PostgreSQL está deliberadamente ausente del archivo. Se ejecuta en el VPS como un servicio normal del sistema y escucha en 172.17.0.1:5432. Es la misma separación por capas que una pila arr en Docker Compose, con la base de datos fuera de Docker.
Mantenga la clave privada de WireGuard fuera del archivo compose. ${WIREGUARD_PRIVATE_KEY} lee de un archivo .env situado junto a él, según el patrón descrito en archivos env y secretos para Docker Compose. La cláusula condition: service_healthy usa el healthcheck que la imagen gluetun ya incluye, por lo que nada se inicia hasta que el túnel informa de que está activo. Los healthchecks de Compose explican la estructura general.
Publicar en todas las direcciones en lugar de sólo en la tailnet
Elimine el prefijo de dirección y las vinculaciones de puertos en 0.0.0.0, que incluye la IP pública del VPS. Hágalo sólo detrás de un firewall que controle y lea primero la nota sobre ufw anterior.
ports:
- "8080:8080/tcp"Confirme que el túnel sigue transportando tráfico
Ejecute la misma solicitud dos veces: una desde dentro del espacio de nombres y otra desde el host, y compare los resultados.
docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.orgLa primera debería mostrar la dirección de salida de su proveedor de VPN. La segunda debería mostrar la dirección del VPS. Si coinciden, el tráfico del contenedor no está pasando por el túnel. Hasta corregirlo, el resto de esta guía no es aplicable.
La tabla de enrutamiento muestra qué tráfico usa el túnel y cuál no.
docker run --rm --network=container:gluetun alpine:3.22 ip route showLa ruta predeterminada debería apuntar a la interfaz del túnel, tun0. Debajo debería aparecer una ruta por cada entrada de FIREWALL_OUTBOUND_SUBNETS, con destino a la puerta de enlace del puente de Docker. Cualquier otra ruta que salga por eth0 es tráfico que omite la VPN.
El servidor de control de Gluetun informa de la misma IP pública en el puerto 8000, en /v1/publicip/ip. Las versiones recientes requieren configurar la autenticación para las rutas del servidor de control. Configúrela antes de depender de ellas.
La filtración que provoca una subred incorrecta
FIREWALL_OUTBOUND_SUBNETS es un agujero que se abre deliberadamente en el firewall, por lo que el tamaño del agujero determina el tamaño del riesgo. Hay cuatro formas de hacerlo demasiado grande:
0.0.0.0/0envía todo fuera del túnel. Las dos comprobaciones de IP anteriores detectan este problema en la primera ejecución, porque devolverán la misma dirección.- Un rango más amplio que el objetivo. Abrir
10.0.0.0/8para llegar a una máquina en10.0.1.7también abre todas las direcciones que un par de torrent pueda anunciar dentro de ese rango. Escriba10.0.1.7/32. - Un rango que se solapa con las direcciones del propio túnel. La documentación de gluetun advierte que esto hace que gluetun envíe el tráfico VPN a través del bridge, lo que rompe el reenvío de puertos. Compruebe el valor de
WIREGUARD_ADDRESSESantes de abrir cualquier rango privado. 100.64.0.0/10para Tailscale. Son aproximadamente cuatro millones de direcciones abiertas para poder llegar a un solo par. Añada los pares necesarios como entradas/32.
Recuerde que esta configuración cubre todo el namespace. Abrir una subred para que un indexer pueda llegar a un servicio del host también abre la misma subred para el cliente torrent que comparte ese namespace. Vuelva a ejecutar la comprobación de IP pública después de cada cambio en esta variable, porque es la única prueba que muestra si el cambio produjo el resultado previsto.
Qué se interrumpe al reiniciar gluetun
gluetun controla el espacio de nombres, por lo que el ciclo de vida de gluetun es también el ciclo de vida del espacio de nombres. Iniciar un contenedor dependiente mientras gluetun está detenido falla de inmediato:
Error response from daemon: cannot join network of a non running containerReiniciar gluetun sin recrear el grupo provoca un fallo menos evidente. Los contenedores dependientes siguen ejecutándose mientras se reconstruye por debajo el espacio de nombres al que estaban conectados, por lo que docker ps informa que todo está funcionando, pero ningún servicio responde. Después de cualquier cambio en el servicio gluetun, recree todo el grupo en lugar de reiniciar una sola parte.
docker compose up -d --force-recreateLo mismo se aplica a las actualizaciones de la imagen. Descargar una imagen nueva de gluetun y recrear sólo ese servicio deja los demás contenedores apuntando a un espacio de nombres que ya no existe.
FAQ
¿Por qué Docker muestra "port publishing and the container type network mode"?
Porque todavía hay un bloque ports: en un servicio que también define network_mode: service:gluetun. Publicar un puerto añade una regla NAT que reenvía un puerto del host al espacio de nombres de red propio de un contenedor, y un contenedor en este modo no tiene uno. Elimine el bloque ports: de ese servicio y añada el mismo mapeo al servicio gluetun. El número de puerto no cambia, porque la aplicación sigue escuchando en él dentro del espacio de nombres compartido.
¿Cómo acceden otros contenedores a un servicio que está detrás de gluetun?
Los contenedores que están dentro del mismo espacio de nombres se conectan entre sí mediante 127.0.0.1. Los contenedores que están fuera usan el nombre del servicio gluetun, por lo que http://gluetun:8080 funciona y http://qbittorrent:8080 no. El contenedor de la aplicación no tiene ninguna dirección en una red Docker, por lo que el servidor DNS integrado no tiene nada que resolver para su nombre. No es necesario publicar ningún puerto para esto, siempre que ambos contenedores compartan una red de Compose.
¿Qué debo incluir en FIREWALL_OUTBOUND_SUBNETS?
Sólo las direcciones a las que un contenedor detrás de gluetun debe iniciar una conexión, expresadas con la mayor precisión posible. Una sola máquina se expresa como /32. Las dos entradas habituales son el host Docker en 172.17.0.1/32 y un /32 por cada par de Tailscale al que se conecte. No añada nunca 0.0.0.0/0 ni un rango que se solape con las direcciones de túnel propias de la VPN. Las conexiones entrantes a un puerto publicado no necesitan ninguna entrada aquí.
¿Por qué el contenedor no puede resolver mis nombres MagicDNS de Tailscale?
MagicDNS funciona configurando el resolvedor del host para que use el servidor DNS de Tailscale, pero el contenedor no usa el resolvedor del host. Usa lo que indique su propio /etc/resolv.conf, que detrás de gluetun es la configuración DNS de gluetun. Confírmelo con docker exec <container> cat /etc/resolv.conf. Use la dirección numérica 100.x del par o fije el nombre mediante una entrada extra_hosts en ese contenedor.
¿Cómo confirmo que el tráfico sigue pasando por la VPN?
Ejecute una solicitud desde dentro del espacio de nombres y la misma solicitud desde el host; después, compare las respuestas. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org debería devolver la dirección de salida de su proveedor de VPN, mientras que curl -s https://api.ipify.org en el VPS devuelve la dirección del VPS. Dos respuestas coincidentes indican que el túnel no está transportando el tráfico del contenedor. Repita esta comprobación después de cada cambio en FIREWALL_OUTBOUND_SUBNETS.