I’ve been mining XMR for some time now with the purpose of using it to purchase a Mullvad VPN subscription. I’m finally to that project, and been putting it off and putting it off. Finally decided to tackle it.
I’ve got several ideas for the VPN itself, not least of which is putting the XMR rig behind it. But first priority is setting up a torrent client bound to the VPN interface. For downloading Linux ISOs and cars, obviously.
I ran through this last night and was far more successful than I expected, given how many issues I ran into unique to my setup, and how many rabbit holes I went down. Going to walk through the setup and my final config, for future me.
First requirement - qBittorrent web UI
My server is a Debian headless machine running all Docker services behind an nginx reverse proxy, and I’d like to keep it that way. Forget using RDP for a torrent client interface.
I’ve used transmission as a torrent client in the past, but it does not provide the functionality to bind to an interface like qBittorrent does. To me that’s a must.
I could also use a qBittorrent client on my laptop, and transfer everything, but I don’t have an NFS share set up to be able to mount my NAS storage remotely, so I’d rather do it directly on the server.
That leaves me with finding a qBittorrent web UI client, which luckily, exists (2).
This requirement ended up causing several other difficulties down the line, which I’ll get into. But the setup process was mostly painless.
Here is the initial Docker compose file:
services:
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
environment:
- PUID=1001
- PGID=1002
- TZ=Etc/UTC
- WEBUI_PORT=8082
- TORRENTING_PORT=6881
volumes:
- /path/to/appdata:/config
- /path/to/downloads:/downloads
ports:
- 127.0.0.1:8082:8082
- 6881:6881
- 6881:6881/udp
restart: unless-stopped
I changed the default web UI port and a couple other things (timezone, filepaths, PUID/PGID) to match my specific setup.
The nginx proxy config was the first issue I ran into, here’s the final one:
server {
server_name qbit.domain.com;
location / {
proxy_pass http://127.0.0.1:8082;
}
listen 443 ssl;
include snippets/domain-com.conf;
include snippets/ssl-params.conf;
proxy_set_header Host $proxy_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
}
The proxy_set_header
values were necessary for 1) the page to even look right and 2) the authentication to work. Thanks to here for those.
Now I had a working qBittorrent client on my server accessible via web browser over a local subdomain. Next order of business was to set up the VPN interface and bind it to qBittorrent.
Initial VPN setup
So, actually getting the VPN onto the server was easy. I’ve got some experience with setting up WireGuard via CLI, not too difficult. Mullvad will generate configuration files for you.
Initially tried WireGuard, but it’s funny how it works. I didn’t dig into it enough to figure out if you can pass specific traffic by binding an interface, as opposed to catching ALL traffic by IP address (the AllowedIPs
line in the configuration). When I ran wg-quick up wg0
, all server traffic was directed to the VPN and I don’t want that to be the case.
So I moved on to OpenVPN. Again, configuration was easy, it was two commands after downloading the configuration files from Mullvad:
sudo apt install openvpn
sudo openvpn --config mullvad_<LOCATION>.conf
(do this from the configuration directory)
This seemed to do what I needed: creating a network interface I could bind to the qBittorrent. Now the problem: qBittorrent is running in a Docker container, and doesn’t see the VPN interface, by design. I could only select eth0
and lo
for the interface inside qBittorrent. I had a suspicion this would be the case when I started this project.
So it was back to the drawing board. By that I mean searching the internet for the inevitable kind soul who has a similar enough configuration to mine and has documented the process, or started a reddit thread.
Figuring out how to bind VPN interface to Docker
This configuration ended up being an entirely different setup than I had envisioned. I figured the process would be similar to passing through a physical NIC in Proxmox to a VM running. The best lead I found was actually to install another Docker container, a VPN interface, called Gluetun, and use that as your VPN instead of installing WireGuard or OpenVPN to the bare metal machine.
Huge thanks to these posts (a third), I basically copied the configuration 1:1.
But this section of the post ended up leading me down another rabbit hole - Portainer.
Setting up Portainer
I have 10+ Docker containers running at all times, managed via compose files, a custom updater script that runs once a week via cronjob, and good ol manual CLI. This setup has been helpful to me, to get at least a baseline of how to manage Docker containers manually, but it’s also getting to be a massive pain in the tush. I had heard of Portainer before, but the post got me curious again.
At this point it was almost 1am, and I really just wanted to get this working and go to bed. But I decided to try a Portainer install via compose file, and see if it worked. I’ve done the compose file and reverse proxy config so many times now that it’s really quick to get a new subdomain up and running. So I did!
I thought I’d just spin it up and see what it looked like, then shut it down and figure it out at a later date. I assumed I would need to deploy the Docker containers again and/or point them to the Portainer interface in some manner, but right after install the containers were all there! This will end up being a godsend I imagine. Definitely a bonus.
Funnily enough, the reason I set it up initially was to use the console functionality, but that just doesn’t work with any container I’ve tried so far. I ended up just falling back to docker exec
.
Setting up Gluetun
Turns out I didn’t really need Portainer for any of this part of the process. I downloaded Gluetun, edited its config accordingly, edited the qBittorrent config accordingly, restarted both containers, and bam.
Here’s my final Gluetun Docker compose:
---
services:
gluetun:
image: qmcgaw/gluetun
container_name: gluetun_qbit
cap_add:
- NET_ADMIN
ports:
- 127.0.0.1:8082:8082
- 6081:6881
- 6081:6881/udp
volumes:
- /path/to/gluetun/dir:/gluetun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=<wg private key>
- WIREGUARD_ADDRESSES=<wg address>
- TZ=Etc/UTC
And the edited qBittorrent Docker compose:
services:
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
environment:
- PUID=1001
- PGID=1002
- TZ=Etc/UTC
- WEBUI_PORT=8082
- TORRENTING_PORT=6881
volumes:
- /path/to/appdata:/config
- /path/to/downloads:/downloads
network_mode: "container:gluetun_qbit"
# ports:
# - 127.0.0.1:8082:8082
# - 6881:6881
# - 6881:6881/udp
restart: unless-stopped
Note the only change being commenting out the ports and changing the network mode to “container:gluetun_qbit
”. Now the qBittorrent container depends on the Gluetun container being up and running.
Setting up Gluetun for other Docker containers
I immediately saw applicability for other containers I was already running, and want to put behind a VPN. Essentially followed the same process as I did for the qBittorrent container, just for a different Docker container:
- Create another Gluetun container with a different name
- Adjust the ports as necessary
- In the applicable Docker container, comment out the ports and change the network mode accordingly
Verifying VPN connectivity
sudo docker exec -it <container-name> curl -sS https://ipinfo.io/json
Should display your VPN information and NOT your home IP address.
Bonus - combining Gluetun and qBittorrent Docker compose files
An issue I ran into very early on in the process was my updater script (calling it a script is perhaps a stretch…it is a for
loop that cd
’s into the Docker compose directories and runs docker compose pull && docker compose up -d
). After the script ran, both containers were down.
I’m not sure why the Gluetun container went down, but the qBittorrent container depends on the Gluetun network being up to run. Combining them into a single compose file seems to have worked to fix that issue.
Obviously as someone who is still very new to Docker, I can’t speak for whether this is good practice or sensible. The two containers are still distinct, they just get started as one unit when the updater script runs now.
Final thoughts
Very satisfied with how this ended up going. I was dreading this configuration, I thought it was going to be one of those projects that would take weeks off and on, and have an inelegant solution.
Biggest takeaway from this: trust my process. Some simple rules have helped immensely: document thought process, document obstacles encountered, document final configuration/solution. Even if I have to throw that configuration out shortly thereafter, I’ve taken a snapshot of my thought process and approach up to that point. Oftentimes I’ll come back later and integrate some of an earlier idea into a new project.
Crazy how much my changes in approach have helped me iterate.
EOF