SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-29

Why Docker VPN Container Ports Vanish with Gluetun

Gluetun shared network dey make container ports and service name disappear. See the exact Docker error, correct compose setup, and why v3.41.3 matter.

Why ports dey disappear when you route Docker containers through VPN

To route Docker containers through VPN, you give one container the tunnel, then attach the others to its network namespace with network_mode: "service:gluetun". Na this attachment dey surprise people. The attached container no longer get network of its own, so its published ports and Docker service name disappear too. Publish the ports for the VPN container instead, and other containers fit reach the app through the VPN container name.

If you leave a ports: block for the attached container, Docker go refuse to create am at all:

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

The tool wey dey use here na Gluetun, a container wey connect to commercial VPN (virtual private network) provider through WireGuard or OpenVPN and carry its own firewall. Release v3.41.3 na the current one as of August 2026. The examples use Mullvad with WireGuard, so you need account and key from your provider. If you prefer make hardware wey you own terminate the tunnel, running your own WireGuard server on a VPS go build the other end, while wg-easy for Docker go wrap am with web interface.

Wetín network_mode: "service:gluetun" really dey do

Every Docker container normally dey get im own network namespace: im own interfaces, routing table, firewall rules and listening sockets. service: mode dey skip that step and start the container inside gluetun namespace. One namespace mean one IP address, and this one change six things.

  • The app no get im own address. Na gluetun address be im address.
  • The app no attach to any Docker network, so dem no register im service name and e no go resolve. Other containers must use gluetun.
  • Containers wey dey inside the namespace fit reach each other through localhost.
  • Two containers for one namespace no fit listen on the same port. Gluetun documentation talk am clearly: no workaround dey.
  • Capabilities belong to container, not namespace. Gluetun get NET_ADMIN and /dev/net/tun because na im create the tunnel interface. The attached container no inherit dem.
  • Compose reject any file where one service set both network_mode and networks. Attach gluetun to your networks, and the app go follow am.

If you restart gluetun, e go disconnect everything wey attach to am. Na documented behaviour be this, and na why gluetun dey restart the VPN process inside the container instead of exiting when the connection fail. After you restart or recreate gluetun yourself, restart the containers wey attach to am.

Compose file wey dey work

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

The :v3 tag na the newest stable release for v3 series. The :latest tag point to the last commit for master branch, wey be the development edge, so pin :v3 for machine wey you no wan debug on Tuesday.

WEBUI_PORT=8080 gats match the published port, because qBittorrent dey bind inside gluetun namespace and the publish rule dey send host traffic go port 8080 there. If you change one number without changing the other, the port no go answer anything. 127.0.0.1:8080:8080 keep the web interface for host loopback address. Bare 8080:8080 go publish for every interface and write its own firewall rule, and na so Docker published ports dey bypass ufw directly.

Bring am up, then check am for this order:

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

docker compose ps suppose show gluetun as healthy and qbittorrent as running. Then confirm the exit address from inside the namespace. Na this check dey decide everything else:

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

The ip field for that JSON suppose be your VPN provider address. If na your server own address, the app no dey inside the tunnel, and nothing for down go behave as described.

Key dem no suppose dey inside compose file

gluetun.env dey hold the login details, and e no dey enter git:

WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32

Both values come from one WireGuard configuration file wey you generate for your provider account area. Set the file mode to 600. Make you understand wetin this arrangement provide: the key no dey inside your repository, but docker inspect gluetun still dey print every environment variable to anybody wey fit reach the Docker socket. Environment files and secrets for Docker Compose explain the stronger options.

How container wey dey outside tunnel fit talk to one wey dey inside am

Both directions dey work, and each one dey use different name. The two containers need shared Docker network. That network na gluetun's network, because the attached container no get its own network. How Docker Compose networks dey wired explain the default setup.

From outside go inside, use gluetun's name and the port wey the app dey listen on. A reverse proxy container fit reach qBittorrent web interface for gluetun:8080. You no need ports: entry for this, because traffic between containers dey remain for Docker network and e never touch host port.

From inside go outside, use the other container's service name, postgres:5432 for example. Gluetun don dey resolve other container names from inside its namespace since v3.41, so pin that version or newer if one name no gree resolve.

Gluetun firewall dey decide who fit open connection to am. Traffic from gluetun own Docker network dey allowed. If client dey another subnet, like laptop for your LAN or container for separate bridge network, firewall go drop am until you name that subnet:

FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24

The documented meaning clear: na comma-separated subnets wey Gluetun and containers wey dey share its network stack fit access.

Inbound connections from internet na separate problem. Torrent client peers dey enter from VPN side, so publishing port 6881 for host no go do anything for dem. You need forwarded port from your provider, and list that port inside FIREWALL_VPN_INPUT_PORTS. This setting allows ports from VPN server side. Na this part most media stacks wey Docker Compose build leave broken.

The kill switch: when tunnel drop, wetin go happen

This setup show why e complex when failure happen. The attached container no get second route. The namespace wey e share na the only way e fit comot from the machine. So when tunnel dey down, e no get any fallback path. Gluetun firewall enforce the same rule from the other side: outbound traffic must pass through tunnel or go to VPN server endpoint. E drop every other traffic. No time dey when packets fit leak through plain interface while client dey reconnect.

Gluetun dey monitor its own connection. Every minute, e sends ICMP echo (a ping) go the addresses for HEALTH_ICMP_TARGET_IPS. By default, dem na 1.1.1.1,8.8.8.8. Every five minutes, e makes complete TCP and TLS (transport layer security) connection go HEALTH_TARGET_ADDRESSES. The default na cloudflare.com:443,github.com:443. When these checks fail, e restarts VPN inside the container and logs the event:

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

Read the attached container logs with this order for mind. Lines like connection refused, operation not permitted and i/o timeout inside the app na consequences of dead tunnel, no be the causes. Gluetun documentation talk this one clear, because people dey report the consequence and spend hours chasing the wrong problem.

HEALTH_RESTART_VPN=on na the default and e suppose remain on. Turn am off only when you dey debug one specific failure, because when e dey off, dead tunnel go remain dead.

Ordering: make stack no start before tunnel dey up

The image get one Docker healthcheck:

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

That command dey run another short-lived copy of gluetun. E dey query the health server of the one wey dey run for http://127.0.0.1:9999/. Tunnel wey dey work go answer 200 OK. Broken one go answer 500 Internal server error with error string, and container go mark unhealthy after just one failure.

condition: service_healthy na wetin dey wait for that. Plain depends_on: [gluetun] dey wait only for container to start. Container fit start several seconds before handshake complete. So app go start with dead network and e often go give up on the first connection attempt. Healthchecks for Docker Compose dey explain the syntax and timing fields.

One limitation dey catch people. Compose dey evaluate that condition once, when e create the container. E no dey stop or restart the app later if gluetun turn unhealthy. Gluetun internal auto-healing dey handle that case instead. Na why e restart the VPN process, no be the container.

Check whether DNS leak dey happen before you trust the setup

DNS (domain name system) na the leak wey fit remain even when tunnel correct. Gluetun dey run its own resolver inside the namespace and e dey forward queries over DoT (DNS over TLS) go Cloudflare by default: DNS_UPSTREAM_RESOLVER_TYPE=dot and DNS_UPSTREAM_RESOLVERS=cloudflare. Leave both as dem be, and your lookups go dey encrypted and pass through the tunnel.

The setting wey fit spoil this na DNS_UPSTREAM_PLAIN_ADDRESSES. People dey use am when name no gree resolve and dem want make their router or provider's resolver answer instead. Gluetun documentation talk the cost clear: all DNS traffic no go pass through the VPN tunnel and e go leak outside am. Your traffic still dey private. But your list of hostnames no dey private. The WireGuard version of this same mistake dey covered for DNS wey stop resolving over a WireGuard tunnel.

To test am, set HTTPPROXY=on for gluetun and publish 8888:8888/tcp, then point browser go that proxy and load a DNS leak test. The result suppose show your provider or Cloudflare, never your home router. Gluetun documentation warn say some leak tests fit show strange results, because the resolver inside the namespace na local caching intermediary, no be the server wey finally answer. Treat wrong country or your own ISP's resolver as the real signal.

Tailscale add beside VPN sidecar, and which one go win

Tailscale na overlay network wey build on WireGuard to reach your own machines. People dey run am beside provider VPN to keep admin path enter the stack. The two no dey fight often, and na reason wey worth understanding dey behind am. Tailscale documentation talk say default behaviour be this: e act as overlay network, e only route traffic between devices wey run Tailscale, and e no touch your public internet traffic.

So, the answer depend on one setting.

  • Tailscale for im own container, with default configuration: e no ever see the app outbound traffic. Gluetun carry all of am. Tailscale fit reach the app for gluetun:8080, exactly like any other outside container.
  • Tailscale attach to gluetun namespace with network_mode: "service:gluetun": e need im own cap_add of net_admin and net_raw, because capabilities no dey come with the namespace. For default userspace networking mode, TS_USERSPACE dey on. tailscaled no create any interface and e work as SOCKS5 or HTTP proxy, so e no fit change routing. Gluetun still carry everything.
  • The same setup, with TS_USERSPACE=false: tailscaled create tunnel device and install routes, but na only for tailnet range 100.64.0.0/10 plus any subnet routes wey you advertise with TS_ROUTES. Public traffic still comot through gluetun.
  • Any of the setups above with exit node selected, sudo tailscale set --exit-node=<exit-node-ip>: Tailscale claim the default route and win. No combine am with gluetun. One default route, one owner.

If na those advertised routes be the main point, and you want make the whole private network behind the box reachable instead of only the box, run Tailscale subnet router on a VPS cover route approval, IP forwarding, and the client-side flag wey TS_ROUTES leave undone by itself.

If you use Tailscale to give yourself admin URL instead of route, tailscale serve and tailscale funnel put HTTPS in front of gluetun:8080 for your tailnet. Only funnel open am to public internet.

One side effect dey show when Tailscale run inside the tunnel. Its peers see the VPN provider address, so e fit fall back to relays more often. tailscale status show relay "..." beside a peer instead of direct when this happen. The connection still work, but e slower. If na only the overlay you really need, difference between plain WireGuard and Tailscale na better place to start.

Wetin fit spoil, and the message wey you go see

Docker no gree create the app container. Error response from daemon: conflicting options: port publishing and the container type network mode mean say one ports: block still dey for the attached service. Move am go gluetun.

Compose no gree accept the whole file. Service no fit set both network_mode and networks. Put the networks for gluetun.

Another container no fit resolve the app. curl: (6) Could not resolve host: qbittorrent na correct behaviour, because the attached container no join any network and e no register any name. Use gluetun and the port.

The second attached container no go start. Two processes for one namespace no fit bind the same port. The one wey lose go report say the address don already dey use. Change the app internal port, or run another gluetun.

The app no get network after you touch gluetun. When you restart or recreate gluetun, e go cut connectivity for every container wey attach to am. Restart those containers.

Small pages dey load, but large ones dey hang. Na MTU (maximum transmission unit) cause am. The tunnel add extra overhead, and something for the path dey drop packets wey too large without sending error back. Lower WIREGUARD_MTU, try 1400, then 1320.

Gluetun no ever become healthy. The startup check dey name the first things to check: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. Check whether the key don expire, then check whether the server list stale, then check whether your host firewall dey block outbound UDP.

FAQ

Why my container publish ports stop working behind Gluetun?

Because network_mode: "service:gluetun" dey put the container inside gluetun network namespace, and namespace get one IP address and one set of listening ports. The app still dey listen, but publish rule must dey on the container wey own the namespace. Move the ports: list go the gluetun service. If you leave am for the attached service, Docker no go even create am: Error response from daemon: conflicting options: port publishing and the container type network mode.

How I fit reach container wey dey inside VPN tunnel from container wey dey outside am?

Use gluetun service name and the port wey the app dey listen on, for example gluetun:8080. The attached container no get Docker network of its own, so its own name no go ever resolve. Nothing need publishing for container-to-container traffic. If you dey go the other way, container inside the namespace fit reach outside container with its service name, like postgres:5432, for Gluetun v3.41 and newer. Client wey dey another subnet, like laptop for your LAN, gluetun firewall go drop am until you add that subnet to FIREWALL_OUTBOUND_SUBNETS.

Gluetun dey work as kill switch when VPN drop?

Yes, and na two reasons dey cause am at the same time. The attached container no get route except the one for the shared namespace, so dead tunnel leave am without path comot from the machine. Gluetun firewall also allow outbound traffic only through the tunnel and go the VPN server endpoint. Gluetun go restart the VPN internally and log WARN [vpn] restarting VPN because it failed to pass the healthcheck, instead of exiting, because every attached container lose network when gluetun itself restart.

Tailscale and Gluetun for the same stack: which one carry outbound traffic?

Gluetun, for every configuration except one. By default, Tailscale only route traffic between devices for your tailnet and leave public traffic alone. For the container image default userspace mode, e no create interface at all, so e no fit affect routing. With TS_USERSPACE=false, e install routes for 100.64.0.0/10 and your advertised subnets only. The exception na exit node: sudo tailscale set --exit-node=<exit-node-ip> make Tailscale the default route, and then Tailscale win. Choose one product to own the default route instead of stacking both.