Blog

  • Private torrenting using Transmission, Gluetun and Surfshark

    Private torrenting using Transmission, Gluetun and Surfshark

    I’ve been ruminating on establishing an account with privacy‑focused VPN for months.

    Not in the casual ‘maybe someday’ way, but in the slightly paranoid, way that has me planning changes to my home networking stack.

    For about two years I’ve had a respectable ‘Linux ISO archival system’ with the help of my good friends Sonarr and Radarr. In that time Transmission, my preferred torrent client, has quietly and without fanfare downloaded many, many gigabytes of diverse Linux distributions.

    And somehow — through what I can only assume is good fortune and the benign incompetence of my ISP — I have never been pinged for my somewhat outsized torrent activity. 

    That said, it would be unwise to push my luck indefinitely: so a privacy VPN would be an invaluable to diffusing the gory details of exactly which Linux ISOs I am mirroring day-to-day.

    However, I don’t want to run my whole home network through a VPN, so installing Wireguard on my router isn’t the answer. I don’t want to deal with the complexity of routing every packet through a tunnel, nor the inevitable performance penalties that would come with that solution.

    I want something much more targeted:

    • Only torrent traffic should use the VPN  
    • The rest of the network should remain untouched  
    • Compatible/leverages existing docker-based stack 
    • Fail-safe: VPN disconnection kills torrent client.

    If the tunnel disappears, the torrent client should lose connectivity entirely rather than accidentally leaking traffic through my ISP connection.

    Tailscale

    As everyone knows (because I’ve sung its praises to everyone I’ve ever met) I’m a devoted Tailscale fan. For a while I experimented with trying to route Transmission’s traffic through Mullvad-exit nodes using Tailscale’s integration. In theory, this could have solved the problem long-term.

    In practice, the Tailscale-Mullvad path was difficult to set up and felt bolted-on rather than purpose-designed. And ultimately, as an identity-aware network, Tailscale couldn’t offer privacy in the same way that a privacy-VPN on its own could.

    A better approach would be to give my torrent stack its own dedicated VPN connection.

    Choosing Surfshark

    After a fairly tedious comparison of pricing, speed claims, logging policies and Reddit arguments, I impulse-bought a few years of Surfshark on sale for about $2 per month.

    A few factors made the decision easier:

    • Surfshark’s technically competent implementation of their ‘no logs’ policy, independently audited 
    • Good support for WireGuard
    • Local servers in Melbourne with 10 gigabit all around the world.

    Running a VPN endpoint close to home dramatically reduces latency compared with tunnelling traffic halfway around the world.

    Enter Gluetun

    The solution turned out to be Gluetun.

    Gluetun is a specialised VPN container designed to sit in front of other containers and route their traffic through a VPN tunnel. It supports multiple providers, including Surfshark, and provides built‑in firewall rules that ensure traffic cannot escape if the VPN connection drops.

    Instead of running the torrent client directly on the Docker host, the torrent client runs inside the Gluetun network namespace.

    If Gluetun cannot reach the VPN provider, the torrent container cannot reach the internet.

    My Existing Media Stack

    Before integrating the VPN, my Docker stack already had a fairly tidy structure.

    Transmission serves as the starting point for what I call the “mediamanagers” network. All of the other media tools — Sonarr, Radarr, and friends — connect to this network as an external Docker network.

    Transmission exposes its web interface on port 9091, which the media managers use to control downloads.

    Introducing Gluetun required a new networking pattern that I hadn’t used before.

    Gluetun as a Network Service

    The key concept is that Gluetun becomes the network provider for another container.

    Instead of Transmission having its own network configuration, it shares Gluetun’s network stack.

    Docker accomplishes this using:

    network_mode: "service:gluetun"

    When Transmission starts, it effectively lives inside the Gluetun container from a networking perspective.

    This means:

    • All internet traffic goes through the VPN  
    • Transmission itself has no direct network access  
    • If Gluetun stops, Transmission loses connectivity  

    Synology-specific requirement to activate the TUN kernel module.

    Because Synology does things differently, to use most VPNs without significant user-space performance penalties you need to activate a dormant kernel module, /lib/modules/tun.ko.

    The best way to handle this is to set up the below script as a startup task to be run by the root user.

    #!/bin/sh -e
    
    /sbin/insmod /lib/modules/tun.ko

    Connectivity Puzzle

    Once Transmission shares Gluetun’s network namespace, it no longer exposes ports directly. Instead, the ports needed by Transmission need to be exposed by the Gluetun container, which proxies the function back to Transmission.

    service:
      transmission:
        network-mode: "service:gluetun"
      gluetun:
        ports:
          - 9091:9091 #Transmission web UI
          - 51820:51820/tcp #torrent peer port
          - 51820:51820/udp #torrent peer port

    Verifying the Kill Switch

    Start the stack with docker compose up -d, then simulate a VPN outage by stopping the Gluetun container docker container stop gluetun.

    Transmission should immediately lose connectivity and downloads should stop. No fallback to the host or mediamanagers network should occur.

    Restart the Gluetun container docker start gluetun and downloads should resume once the tunnel reconnects.

    Verifying the External IP Address

    Check the external IP from inside the Gluetun container:

    docker exec gluetun curl ifconfig.me

    Compare with the host:

    curl ifconfig.me

    The two addresses should differ, confirming traffic is exiting via the VPN.

    Using Gluetun as a VPN Gateway

    Additional containers can also be routed through the VPN simply by using:

    network_mode: service:gluetun

    Any container attached this way inherits the same VPN tunnel and firewall protection.

    Final Thoughts

    Once configured, the setup turns out to be elegant and reliable. Gluetun acts as a reusable VPN gateway with built‑in firewall protection and a clean Docker networking model.Most importantly, it ensures that my ever‑growing archive of Linux distributions can continue expanding — safely routed through a dedicated VPN connection.

    compose.yaml

    services:
      transmission:
        image: lscr.io/linuxserver/transmission:latest
        container_name: transmission
        environment:
          - PUID=1000 #Change to suit your setup
          - PGID=1000 #Change to suit your setup
          - UMASK=022 #Remember, UMASK is subtractive
          - TZ= #Pick your most appropriate TZ
          - TRANSMISSION_WEB_HOME=  #optional
          - USER= #optional, username for simple auth
          - PASS= #optional, password for simple auth
          - WHITELIST= #optional
          - PEERPORT= #optional
          - HOST_WHITELIST= #optional
        volumes:
          - ./config:/config
          - /volume1/media/Downloads/transmission/:/data/downloads
        restart: unless-stopped
        network_mode: "service:gluetun"
    
      gluetun:
        container_name: gluetun
        image: qmcgaw/gluetun
        cap_add:
          - NET_ADMIN					
        devices:
          - /dev/net/tun:/dev/net/tun
        environment:
          - VPN_SERVICE_PROVIDER=surfshark
          - VPN_TYPE=wireguard
          - WIREGUARD_PRIVATE_KEY=
          - WIREGUARD_ADDRESSES=
          - SERVER_COUNTRIES=
        ports:
          - 9091:9091 #Transmission WebUI
          - 51413:51413 #Torrent Peer Port
          - 51413:51413/udp	#Torrent Peer Port
        networks:
          - mediamanager
    
    networks:
      mediamanager:
        name: mediamanager
        driver: bridge
        enable_ipv6: false
        attachable: true
        ipam:
          driver: default
          config:
            - subnet: 10.10.2.0/24
              ip_range: 10.10.2.2/24
              gateway: 10.10.2.1
  • Hello again, world!

    After a some extended downtime I finally feel confident enough spinning up Docker and getting some kind of website back up and running on this domain.

    I used to use this space to document all the things I was learning day to day from ‘stuffing around on the internet’ and I may continue in that place.

    For now, here’s the compose.yaml which brings you this site. That means this post is officially documentation. For now, that’s a win.

    services:
      wordpress:
        image: wordpress:latest
        depends_on:
          - db
        env_file: .env
        volumes:
          - ./wordpress_data:/var/www/html
        networks:
          - wordpress_network
          - reverseproxy
    
      db:
        image: mysql:5.7
        env_file: .env
        volumes:
          - ./db_data:/var/lib/mysql
        networks:
          - wordpress_network
    
    networks:
      wordpress_network:
      reverseproxy:
        external: true
        name: npm-network