SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor

Gluetun Port Forwarding for Torrent Apps Setup

Downloads dey work but no connection dey enter? Learn gluetun port forwarding, update your torrent client after every reconnect, and verify the new port.

Why nothing dey connect enter without forwarded port

Gluetun port forwarding dey ask your VPN provider to map one public port for its exit address back to your container. Na only this way another peer fit start connection to your torrent client. Without this mapping, the tunnel dey healthy, downloads dey run, and nothing dey arrive by itself. Every connection wey dey work na one wey your client open first.

The mechanism na NAT (network address translation). Your container dey share the provider's exit address with many other customers. When your client open connection go outside, the provider record that flow and send the replies back through your tunnel. If another peer try connect enter, e no match any recorded flow. So the packet reach the exit address and dem drop am there. Your client still fit reach every peer wey fit accept connections by itself. So downloads go finish and the problem fit remain hidden. Na when you dey seed the problem dey show, because seeder na machine wey other people connect to.

Open inbound port dey change two things. You join swarm faster because peers wey no fit accept connections by themselves fit now reach you. You fit also upload to those peers at all.

Why most VPN providers no dey offer forwarded port

Forwarded port na scarce resource for shared address. Provider reserve one port number for one exit IP for one customer, then e go answer for anything wey that customer do with am. Several big providers don remove this feature and dem talk say abuse handling na the reason. Treat support like category question, no be checkbox: ask whether provider dey offer port forwarding today, for your plan, and for servers wey you fit actually select.

Where forwarding dey available, the port dey dynamic. E belong to VPN session, no be your account, so e fit change to different number after every reconnect. Private Internet Access dey issue signed port wey gluetun dey refresh, and the upstream documentation talk say you go keep the same port for 60 days as long as you bind mount the /gluetun directory so the state fit survive restart. ProtonVPN dey assign random port through NAT-PMP (NAT port mapping protocol) on short lease wey you must renew continuously. Na why setting the port once for the client no dey keep working.

Wetin be the providers wey gluetun fit ask for port

As of gluetun v3.41.3, wey release for 30 July 2026, the native integration dey validate four provider names: Private Internet Access, ProtonVPN, Perfect Privacy and PrivateVPN. Enable am with VPN_PORT_FORWARDING=on, wey be off by default. Older guides dey use PORT_FORWARDING or PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING. Both still work for this version as retro-compatible names, but dem dey phase out.

Two provider details dey decide whether the request fit succeed at all. ProtonVPN need paid plan, and you must switch on NAT-PMP: enable NAT-PMP (Port Forwarding) under the VPN options when you generate the WireGuard configuration, or append +pmp to your username when you use OpenVPN. Private Internet Access for OpenVPN get PORT_FORWARD_ONLY, wey restrict server selection to servers wey support forwarding, so you no go land for one wey never support am. WireGuard and OpenVPN dey differ for how dem request the port, so read your provider page before you choose.

When gluetun dey run custom configuration instead of built-in provider, VPN_PORT_FORWARDING_PROVIDER dey specify the API wey gluetun suppose call. The upstream Private Internet Access page pair this variable with VPN_PORT_FORWARDING_USERNAME and VPN_PORT_FORWARDING_PASSWORD, wey carry the account credentials wey the port request need.

Turn on gluetun port forwarding for docker compose

This one assume say the tunnel don already work. If e no work, start with route Docker container traffic through gluetun and come back when downloads don dey run.

services:
  gluetun:
    image: qmcgaw/gluetun:v3.41.3
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    ports:
      - 8080:8080/tcp
      - 8000:8000/tcp
    volumes:
      - ./gluetun:/gluetun
    environment:
      - VPN_SERVICE_PROVIDER=protonvpn
      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
      - VPN_PORT_FORWARDING=on
      - TZ=Etc/UTC
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:5.2.3
    container_name: qbittorrent
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Etc/UTC
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent:/config
      - ./downloads:/downloads
    depends_on:
      - gluetun
    restart: unless-stopped

Pin the tag. qmcgaw/gluetun:latest dey follow the master branch, where dem dey change the port forwarding internals for v4, so image wey no get fixed tag fit change behaviour for the next docker compose pull. Keep the private key comot from the compose file with an env file for compose secrets.

Gluetun dey write forwarded port for where

Gluetun dey expose the port for three places, and all of dem get the same value.

E dey log the port once for every acquisition. The line dey read port forwarded is 45678, and no port forwarded when the request no produce anything.

docker logs gluetun 2>&1 | grep -i "port forwarded"

E dey write the number to the file wey VPN_PORT_FORWARDING_STATUS_FILE name, and the default na /tmp/gluetun/forwarded_port. The file get one port for each line, dem write am with mode 0644, and chown am to the container's PUID and PGID. When forwarding stop, gluetun dey clear the file instead of deleting am, so consumer fit read empty file instead of encountering missing file.

docker exec gluetun cat /tmp/gluetun/forwarded_port

E dey serve the value for the control server, wey dey listen on :8000 by default and HTTP_CONTROL_SERVER_ADDRESS dey set am.

curl -s http://127.0.0.1:8000/v1/portforward
{"port":45678,"ports":[45678]}

Gluetun still dey open that port for im own firewall on the VPN interface, so FIREWALL_VPN_INPUT_PORTS no dey needed while the native integration dey handle the work. That variable dey cover the other case: provider wey gluetun no fit query, where dem issue static port to you out of band and you need allow am by hand.

One of these three dey durable, while the other two no dey. The upstream documentation mark the status file as deprecated for v4.0.0, and GET /v1/openvpn/portforwarded don already answer with 301 Moved Permanently wey dey point to /v1/portforward. New work suppose read the control server.

Why dem must tell client the port for every reconnect

Torrent client dey store its listening port for its own configuration, and e dey keep that number after restart. Forwarded port na property of VPN session. After reconnect, the two numbers no match. Provider go map one port wey nothing dey listen on, while client dey listen on another port wey nothing map. Reconnects no be rare: container restart, server change, dropped tunnel wey gluetun's health check restart, or lease wey no fit renew. The result na setup wey dey reachable yesterday but quietly no dey reachable today, with no error for either log.

So dem must apply the port immediately gluetun acquire am. Two ways dey to connect this setup, and the difference na which process dey do the work.

Option 1: gluetun dey push the port with an up command

VPN_PORT_FORWARDING_UP_COMMAND dey run when port forwarding come up, and VPN_PORT_FORWARDING_DOWN_COMMAND dey run when e go down. Gluetun go replace {{PORT}} (the first port), {{PORTS}} (all of dem, comma separated) and {{VPN_INTERFACE}} (the tunnel interface name, tun0 by default) before e run the command. Shell syntax need explicit /bin/sh -c wrapper. Na the upstream qBittorrent example be this, written as two compose environment entries:

      - VPN_PORT_FORWARDING_UP_COMMAND=/bin/sh -c 'wget -O- -nv --retry-connrefused --post-data "json={\"listen_port\":{{PORT}},\"current_network_interface\":\"{{VPN_INTERFACE}}\",\"random_port\":false,\"upnp\":false}" http://127.0.0.1:8080/api/v2/app/setPreferences'
      - VPN_PORT_FORWARDING_DOWN_COMMAND=/bin/sh -c 'wget -O- -nv --retry-connrefused --post-data "json={\"listen_port\":0,\"current_network_interface\":\"lo\"}" http://127.0.0.1:8080/api/v2/app/setPreferences'

Every field for that call get im own work. listen_port na the new port. current_network_interface bind qBittorrent to the tunnel. When dem set random_port to false, qBittorrent no go choose im own port for the next start. When dem set upnp to false, e no go try map port through router wey no dey there.

This approach get two requirements. qBittorrent web UI must answer on 127.0.0.1:8080 from inside the gluetun container. This one happen automatically when the client share gluetun network namespace. Also, Bypass authentication for clients on localhost (bypass_local_auth) must dey enabled because the command no send credentials. The down command dey there because qBittorrent no always restore the port after disconnection.

The command dey run inside the gluetun container. Alpine build the container, and e come with wget. The image no get curl. If command name binary wey the image no get, e go fail every time port forwarding come up.

Option 2: process wey dey outside gluetun go read the port

The other pattern dey run one small process beside gluetun. E go fetch the port, then push am enter the client through the client own API. Read am from the control server:

port=$(curl -s http://127.0.0.1:8000/v1/portforward | jq -r .port)

Or read the file, if the process fit see am. /tmp/gluetun/forwarded_port dey inside the gluetun container, so sidecar need shared volume wey dem mount for /tmp/gluetun inside both containers. Another option na to point VPN_PORT_FORWARDING_STATUS_FILE to path under volume wey you don already mount.

Authentication matter for here. For v3.41.3, route GET /v1/portforward belong to default role wey dem call public, with auth = "none". So e go answer without credentials, and gluetun logs warning wey start with route GET /v1/portforward is unprotected by default, please set up authentication. Upstream go close this access for later release. Define role now, inside file wey you bind mount for /gluetun/auth/config.toml:

roles = [
  { name = "qbittorrent", routes = ["GET /v1/portforward"], auth = "apikey", apikey = "myapikey" }
]

Generate key with docker run --rm qmcgaw/gluetun:v3.41.3 genkey and send am inside X-API-Key header. HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE do the same work as one JSON-encoded environment variable when you no wan mount file. If you publish Port 8000 without role, anybody wey fit reach am go fit control VPN state. So decide carefully how far e go travel when you dey plan how to reach gluetun from host and other containers.

Choose the up command when client get API wey one wget call fit drive, because e go run exactly once for each event and e no add anything wey need keep running. Choose external process when client need login flow, config file rewrite, or restart. For an arr stack behind one gluetun container, this one usually end as one small poller, because na only torrent client dey use the port.

The trap: sharing the namespace no mean say listening port don set

This failure dey waste pass time. network_mode: "service:gluetun" dey put the client inside gluetun network namespace, so e get the VPN address, tunnel routes, and gluetun firewall rules. None of these things set the client listening port. Gluetun opens the forwarded port for the VPN interface, packets for that port enter the namespace, and if the client dey listen on another port, kernel no get where to deliver dem. Connection go refuse or time out, even though every outbound check dey healthy. The forwarded port and the client listening port na two different numbers, and the main work na to keep dem equal.

Compare dem instead of guessing. Both commands dey run against the same namespace:

docker exec gluetun cat /tmp/gluetun/forwarded_port
docker exec gluetun wget -qO- http://127.0.0.1:8080/api/v2/app/preferences | grep -o '"listen_port":[0-9]*'

One more setting fit send people for wrong direction. VPN_PORT_FORWARDING_LISTENING_PORT redirects inbound traffic from the forwarded port go one fixed local port with iptables. Upstream tell you make you no use am with torrent clients, because the client dey announce its own listening port to trackers and peers, so the swarm go learn the wrong number.

How to prove say forwarded port dey reachable

The client's own connection indicator dey show outbound tracker connections, so e fit look green even when nobody fit reach you. Test am with a listener wey you control, from network wey dey outside the tunnel. Upstream publish one small tool for this. Stop the torrent client first, because two processes no fit bind the same port.

docker stop qbittorrent
docker exec -it gluetun /bin/sh

Inside the container, change amd64 to match your CPU architecture, and 4567 to match your forwarded port:

wget -qO port-checker https://github.com/qdm12/port-checker/releases/download/v0.4.0/port-checker_0.4.0_linux_amd64
chmod +x port-checker
./port-checker --listening-address=":4567"

Now find the exit address wey gluetun dey use. The response na JSON, and the address dey inside the public_ip field.

curl -s http://127.0.0.1:8000/v1/publicip/ip

Open http://<that address>:4567 from device wey no dey on the same VPN. Phone wey dey use mobile data go work. If page show your browser's IP address and user agent, and port-checker log a matching request, this mean inbound TCP dey reach the namespace. If e timeout, e mean say e no reach, and the cause dey above the client. Stop the tool with CTRL+C, leave the shell with exit, then start the client again. This check na for TCP only. DHT (distributed hash table) and uTP traffic dey use UDP for the same port number, but this test no cover dem.

Failure modes and the strings wey you go see

No port line dey for the log at all. Nobody ask for port. Confirm say variable really reach the container with docker exec gluetun printenv | grep PORT_FORWARDING, because setting variable for wrong compose service na common cause.

Gluetun no gree start and e complain about provider. VPN_PORT_FORWARDING_PROVIDER dey validate against the four supported names, so typo go stop the container instead of making e run quietly without forwarding.

The log talk say no port forwarded. Gluetun ask, but provider no return anything. For ProtonVPN, this usually mean say NAT-PMP no dey enabled for the configuration wey you generate, or the plan no include forwarding. For Private Internet Access, e usually mean say the selected server no offer am.

Port arrive, but nothing fit connect enter. Compare the forwarded port with the client's listening port using the two commands above. If dem match, check say client bind to the tunnel interface and say its random-port option dey off, because that option dey rewrite the listening port every time e start.

The up command look like say e no do anything. Run the exact command inside the container to see the error: docker exec gluetun /bin/sh -c '<your command>'. curl: not found na the usual result, because the image ship with wget only.

401 Unauthorized from the control server. You define auth config, but the role no list the route wey you dey call. Routes dey match as method plus path, so role wey list /v1/portforward alone no cover GET /v1/portforward.

Different port for Private Internet Access after every restart. Bind mount /gluetun so saved port state go survive restart. Without that volume, gluetun go request new port every time.

FAQ

Why my torrents dey download but dem never get incoming connections?

If port no dey forwarded, VPN provider no get NAT rule wey go send inbound packets for any port go your tunnel. So connections wey you no start by yourself go drop for the exit address. Downloads still dey work because your client open those connections by itself, and e fit reach any peer wey fit receive connections. Seeding and joining swarms dey suffer because both one depend on other people reaching you. The fix na to use provider wey offer port forwarding, VPN_PORT_FORWARDING=on for gluetun, then apply the port wey result to the client's listening port.

gluetun fit work with port forwarding from any VPN provider?

No. gluetun v3.41.3 get native integration for four providers: Private Internet Access, ProtonVPN, Perfect Privacy and PrivateVPN. Anything outside that list go fail validation for VPN_PORT_FORWARDING_PROVIDER, and the container go stop during startup. If your provider dey give static port through its own control panel, gluetun no fit request am for you. But FIREWALL_VPN_INPUT_PORTS go allow that fixed port pass through gluetun's firewall. Provider policies fit change, so check the current provider page before you buy plan for this.

I need update the port after every reconnect?

Yes, and that update suppose happen automatically. The forwarded port belong to the VPN session. So container restart, server change or failed lease renewal fit produce new number, while the client still get the old port stored for its own configuration. Either make gluetun push am with VPN_PORT_FORWARDING_UP_COMMAND, wey dey run as soon as forwarding come up, or run small process wey read GET /v1/portforward from the control server and write the value into the client through its API.

How I fit check say the forwarded port really dey open?

Run listener for that exact port inside gluetun's network namespace, then connect to am from outside the VPN. Stop the torrent client first so the port go free. Then run the upstream port-checker binary inside the gluetun container with --listening-address=":<port>". Get the exit address from curl -s http://127.0.0.1:8000/v1/publicip/ip and open http://<address>:<port> from phone wey dey use mobile data. If request appear for the port-checker log, e prove say inbound TCP don arrive. Timeout mean say e no arrive, no matter wetin the client's own status icon dey show.

#gluetun#vpn#port-forwarding#docker#torrenting