SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-13

Why Docker VPN Container Ports Dey Disappear

Gluetun sidecar ports fit vanish with network_mode: service. See the exact Docker error, why the shared namespace hide ports, and the working Compose fix.

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 with am. 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 here na Gluetun, a container wey connect to commercial VPN (virtual private network) provider through WireGuard or OpenVPN, and e 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 in Docker wrap am with web interface.

Wetin network_mode: "service:gluetun" dey actually do

Normally, every Docker container 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 wey dey for one namespace no fit listen on the same port. Gluetun documentation talk am clear: no workaround dey.
  • Capabilities belong to container, no be namespace. Gluetun get NET_ADMIN and /dev/net/tun because na im create the tunnel interface. The attached container no inherit dem.
  • Compose go reject any file wey one service set both network_mode and networks. Attach gluetun to your networks, and the app go use the same network connection.

If you restart gluetun, e go disconnect everything wey attach to am. This na documented behaviour, 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.

The 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 dey point to the last commit for master branch, wey be development edge, so pin :v3 for machine wey you no want 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 for 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. A bare 8080:8080 go publish for every interface and write im own firewall rule. Na so Docker published ports fit pass straight through ufw.

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 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 wey dey below go work as described.

Keep the keys out of the compose file

gluetun.env dey hold the credentials, and e no dey inside git:

WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32

Both values come from a WireGuard configuration file wey you generate for your provider account area. Set the file mode to 600. Make you understand wetin this arrangement give you: 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 one container wey dey outside the tunnel dey talk to another one inside am

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

To connect 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 container-to-container traffic dey stay for Docker network and e no dey touch host port.

To connect from inside go outside, use the other container's service name, like postgres:5432. Since v3.41, Gluetun don dey resolve other container names from inside its namespace. So pin v3.41 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. But client wey dey another subnet, laptop for your LAN, or container for separate bridge network, go drop until you name that subnet:

FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24

The documented meaning exact: na comma-separated subnets wey Gluetun and the 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 the host no go do anything for dem. You need forwarded port from your provider, and you need list that port for FIREWALL_VPN_INPUT_PORTS. This one allows ports from VPN server side. Na this part most media stacks wey Docker Compose build leave broken.

Kill switch: wetin dey happen when tunnel drop

This pattern show say e worth the extra complexity when failure happen. The attached container no get second route. The only way e fit comot from the machine na through the namespace wey e share, so when tunnel dey down, e no get anything to fall back on. Gluetun firewall enforce the same rule from the other side: outbound traffic must pass through the tunnel or go to the VPN server endpoint; e drop everything else. No time window dey where packets fit leak through the 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, wey default to 1.1.1.1,8.8.8.8. Every five minutes, e make complete TCP and TLS (transport layer security) dial go HEALTH_TARGET_ADDRESSES, wey default to cloudflare.com:443,github.com:443. When those checks fail, e restart the VPN inside the container and log 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 that order for mind. Lines like connection refused, operation not permitted and i/o timeout inside the app na consequences of tunnel wey don die, no be the causes. Gluetun documentation talk this one clearly, because people dey report the consequence and spend hours dey pursue the wrong problem.

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

Ordering: make sure 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 health server of the one wey dey run for http://127.0.0.1:9999/. Tunnel wey dey work go answer 200 OK. Tunnel wey get problem go answer 500 Internal server error with error string, and container go get marked as unhealthy after one failure.

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

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

Check whether DNS dey leak 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 forward queries through 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 dey cause this problem na DNS_UPSTREAM_PLAIN_ADDRESSES. People dey use am when name no resolve and dem want make their router or provider's resolver answer instead. Gluetun documentation talk the cost clearly: all DNS traffic no go pass through VPN tunnel and e go leak outside am. Your traffic remain private. But your list of hostnames no remain private. The WireGuard version of this same mistake dey covered for DNS wey stop to resolve over a WireGuard tunnel.

To test am, set HTTPPROXY=on for gluetun and publish 8888:8888/tcp, then point browser to that proxy and load DNS leak test. The result suppose name 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.

Adding Tailscale beside the VPN sidecar, and which one go win

Tailscale na overlay network wey dem build on WireGuard to reach your own machines. People dey run am beside provider VPN so dem fit keep admin access into the stack. The two no dey fight often, and na reason wey make sense dey behind am. Tailscale documentation talk say the default be this: e dey act as overlay network, e only route traffic between devices wey dey 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 attached 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 only for the 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.

One side effect dey show when Tailscale dey run inside the tunnel. Its peers go see the VPN provider address, so expect am to 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 slow. If na the overlay you really need, the difference between plain WireGuard and Tailscale na the 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 dey use already. Change the app internal port, or run another gluetun.

The app no get network after you touch gluetun. If you restart or recreate gluetun, e go drop connectivity for everything wey attach to am. Restart those containers.

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

Gluetun never become healthy. The startup check name the first things to suspect: 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 old, then check whether your host firewall dey block outbound UDP.

FAQ

Why my container published ports stop working behind Gluetun?

Na because network_mode: "service:gluetun" dey put the container inside gluetun network namespace, and one namespace get only one IP address and one set of listening ports. The app still dey listen, but the 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 dey on any Docker network of its own, so its own name no go resolve. You no need publish anything for container-to-container traffic. For the other direction, container wey dey inside the namespace fit reach outside container with its service name, such as postgres:5432, for Gluetun v3.41 and newer. Client wey dey another subnet, such as 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 disconnect?

Yes, and na two reasons dey cause am. The attached container no get any route apart from the one for the shared namespace, so dead tunnel leave am without path comot the machine. Gluetun firewall also allows outbound traffic only through the tunnel and to the VPN server endpoint. Gluetun then restarts the VPN internally and logs WARN [vpn] restarting VPN because it failed to pass the healthcheck, instead of exiting, because every attached container loses network when gluetun itself restarts.

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

Na Gluetun, for every configuration except one. By default, Tailscale only routes traffic between devices for your tailnet and leaves public traffic alone. For the container image default userspace mode, e no create any interface at all, so e no fit affect routing. With TS_USERSPACE=false, e installs 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> makes Tailscale the default route, and then Tailscale wins. Choose one product to own the default route instead of stacking both.