From 18e468c88f37c8f82d46c8992868ac68a1d42d3b Mon Sep 17 00:00:00 2001 From: Goyban Date: Mon, 7 Sep 2026 10:41:40 +0200 Subject: [PATCH] added gluetun with qBittorrent and pyload-ng VPN kill switch via shared network namespace, TorGuard WireGuard through gluetun's custom provider. compose.yaml is the maintained version; compose.personal.yaml is the variant actually running, kept as-is. Verified against gluetun's own settings summary: SOCKSPROXY/SOCKSPROXY_PORT are not real variables and the SOCKS5 server never starts; HTTPPROXY_PORT is deprecated; pyload-ng listens on 8000, not 8880. .gitignore: ignore wg*.conf and *.ovpn everywhere, because add gluetun/compose.yaml gluetun/compose.personal.yaml gluetun/.env.example gluetun/README.md .gitignore/init/** would otherwise commit a WireGuard private key dropped in a service's init/ dir. Un-ignore compose.personal.yaml narrowly, so compose.override.yaml stays out. --- .gitignore | 11 ++ gluetun/.env.example | 38 ++++++ gluetun/README.md | 223 ++++++++++++++++++++++++++++++++++ gluetun/compose.personal.yaml | 61 ++++++++++ gluetun/compose.yaml | 95 +++++++++++++++ 5 files changed, 428 insertions(+) create mode 100644 gluetun/.env.example create mode 100644 gluetun/README.md create mode 100644 gluetun/compose.personal.yaml create mode 100644 gluetun/compose.yaml diff --git a/.gitignore b/.gitignore index a7c710f..e4239fc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ # ─── …except the files that define a stack ────────────────────────── !*/compose.yaml !*/docker-compose.yaml +# A service's alternative compose file, published as documentation. Narrow on +# purpose: `!*/compose.*.yaml` would also un-ignore compose.override.yaml, +# which is exactly where people keep local secrets. +!*/compose.personal.yaml !*/.env.example !*/README.md @@ -26,3 +30,10 @@ *.crt secrets/ TODO + +# VPN configs carry private keys. Named explicitly because the `!*/init/**` +# rule above would otherwise happily commit one dropped in a service's +# init/ directory -- which is exactly where this repo's own conventions +# tell you to put runtime config fragments. +wg*.conf +*.ovpn diff --git a/gluetun/.env.example b/gluetun/.env.example new file mode 100644 index 0000000..0da32c6 --- /dev/null +++ b/gluetun/.env.example @@ -0,0 +1,38 @@ +# Copy to .env and adjust. .env is git-ignored; this file is not. +# +# Only TEMPDISK is required -- everything else has a default in +# compose.yaml. (compose.personal.yaml has no defaults at all: it uses +# bare ${PUID} etc., which Compose turns into empty strings rather than +# failing, so with that file set all four.) + +# ─── Identity ─────────────────────────────────────────────────────── +# Owner of the downloads directory and the two config directories. +# id -u ; id -g +#PUID=1000 +#PGID=1000 +#TZ=Europe/Paris + +# ─── Where downloads land ─────────────────────────────────────────── +# The parent of downloads/torrents and downloads/pyload. Point this at +# the disk with room on it -- this is the one path that cannot have a +# sensible default. +TEMPDISK=/mnt/tank + +# ─── Proxy authentication ─────────────────────────────────────────── +# Both proxies are UNAUTHENTICATED unless these are set -- anyone who can +# reach ports 9980/9998 can route traffic through your VPN account. Set +# both (gluetun needs user and password together) and authentication turns +# on for the HTTP and SOCKS5 proxies at once. +#PROXY_USER= +#PROXY_PASSWORD= + +# ─── Ports ────────────────────────────────────────────────────────── +#QBT_PORT=9880 +#PYLOAD_PORT=8880 +#SOCKS_PORT=9980 +#HTTPPROXY_PORT=9998 + +# ─── NOT in this file ─────────────────────────────────────────────── +# Your WireGuard credentials do NOT belong here. They live in wg0.conf +# next to compose.yaml, which is git-ignored (see the README). Do not +# move that file into init/ and do not paste its keys into .env. diff --git a/gluetun/README.md b/gluetun/README.md new file mode 100644 index 0000000..27ed7f6 --- /dev/null +++ b/gluetun/README.md @@ -0,0 +1,223 @@ +# gluetun + qBittorrent + pyload-ng + +A VPN container with two download clients living inside its network stack. The +point is the **kill switch**: qBittorrent and pyload-ng have no network of their +own, so if the tunnel drops they lose connectivity entirely rather than falling +back to your real address. + +Also exposes gluetun's HTTP and SOCKS5 proxies, so other machines and browser +extensions (SwitchyOmega and friends) can route through the same tunnel without +running a VPN client themselves. + +Configured for **TorGuard WireGuard** via gluetun's `custom` provider. + +## Quick start + +```bash +cp .env.example .env +$EDITOR .env # TEMPDISK is the one you must set + +# Get a WireGuard config from TorGuard's config generator, save it as +# wg0.conf NEXT TO THIS FILE. It is git-ignored. Do not put it in init/. +cp ~/Downloads/torguard-wg.conf ./wg0.conf + +docker compose up -d +docker compose logs -f gluetun # watch for the public IP line +``` + +Verify the tunnel is actually carrying traffic before you seed anything: + +```bash +docker compose exec gluetun wget -qO- https://ipinfo.io/ip # should be TorGuard's +curl -x http://localhost:9998 https://ipinfo.io/ip # same, via the HTTP proxy +``` + +## Ports + +Every port is published on the **gluetun** service, because the other two +containers have no network namespace of their own. + +| Host | Container | Purpose | +|------|-----------|---------| +| 9880 | 9880 | qBittorrent web UI | +| 8880 | 8000 | pyload-ng web UI | +| 9980 | 1080 | SOCKS5 proxy | +| 9998 | 8888 | HTTP proxy | + +In `compose.personal.yaml` two of these differ: pyload is published `8880:8880` +against a container that listens on 8000, and the SOCKS5 server isn't running at +all. See the gotchas. + +## Two compose files + +**[`compose.yaml`](compose.yaml)** is the maintained one and what +`docker compose up` uses. + +**[`compose.personal.yaml`](compose.personal.yaml)** is the older variant still +running on my own box. It works and its HTTP proxy is in daily use, so it's kept +as-is rather than tidied — but it has the quirks listed under gotchas below. +Compose won't load it unless you name it: + +```bash +docker compose -f compose.personal.yaml up -d +``` + +What the maintained file changes: + +| Change | Why | +|--------|-----| +| `SOCKS5_ENABLED` / `SOCKS5_LISTENING_ADDRESS` | `SOCKSPROXY*` are not gluetun variables; the SOCKS5 server never starts | +| `HTTPPROXY_LISTENING_ADDRESS` | `HTTPPROXY_PORT` works but logs a deprecation warning | +| `8880:8000` | pyload-ng listens on 8000; its port isn't settable by env var | +| `HTTPPROXY_USER` / `SOCKS5_USER` + passwords | opt-in proxy authentication, off unless set in `.env` | +| `qmcgaw/gluetun:v3.41.3` | pinned; this container holds the tunnel | +| `${PUID:-1000}`, `${TEMPDISK:?…}` | defaults where sensible, loud failure where a value can't be guessed | +| `condition: service_healthy` | gluetun ships a healthcheck; wait for the tunnel instead of racing it | +| `lscr.io/linuxserver/qbittorrent` | matches pyload-ng's registry; same image | + +Same design, same ports, same volumes. + +## Other VPN providers + +Nothing here is TorGuard-specific except `wg0.conf`. gluetun natively supports +around twenty providers, and for most of them you delete the file mount and set +two or three variables instead: + +```yaml + - VPN_SERVICE_PROVIDER=mullvad + - VPN_TYPE=wireguard + - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY} + - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES} + - SERVER_CITIES=Amsterdam +``` + +**Native WireGuard** — the easy path: AirVPN, FastestVPN, IVPN, Mullvad, +NordVPN, ProtonVPN, Surfshark, Windscribe. + +**Native OpenVPN** — every supported provider, usually just `OPENVPN_USER` and +`OPENVPN_PASSWORD`: the list above plus Cyberghost, ExpressVPN, Giganews, +HideMyAss, IPVanish, Privado, Private Internet Access, PrivateVPN, PureVPN, +SlickVPN, TorGuard, VPNSecure, VPN Unlimited, VyprVPN. + +**WireGuard via the custom provider** — Cyberghost, Private Internet Access, +PrivateVPN, PureVPN, TorGuard, VPN Unlimited and VyprVPN have no native +WireGuard support in gluetun, so they take the `wg0.conf` route this stack uses. + +That last line is why this stack looks the way it does: **TorGuard *is* a +built-in provider (`VPN_SERVICE_PROVIDER=torguard`), but only for OpenVPN.** +Wanting TorGuard *and* WireGuard is precisely the case gluetun's own +documentation sends to the custom provider. It isn't a workaround — it's the +documented path. + +Switching provider changes nothing else: the kill switch, the shared network +namespace and both proxies work the same way regardless of who terminates the +tunnel. Check your provider's page in +[the gluetun wiki](https://github.com/qdm12/gluetun-wiki) for its exact +variables. + +## Why it looks like this + +**`network_mode: "service:gluetun"` is the whole design.** qBittorrent and +pyload-ng don't get their own network stack — they share gluetun's. That's what +makes the kill switch absolute: there is no route out except the tunnel, so a +misconfiguration fails closed rather than leaking. The cost is that those +containers **cannot declare their own `ports:`**; Docker refuses it. Every port +they need is published on gluetun instead, which is why the list above looks +like it belongs to the wrong service. + +**gluetun's firewall is on by default.** Confirmed in its startup summary +(`Firewall settings: Enabled: yes`) — you don't have to configure a kill switch, +you have to avoid breaking the one that's already there. + +**`VPN_SERVICE_PROVIDER=custom` plus a mounted `wg0.conf`.** TorGuard isn't one +of gluetun's built-in providers, so rather than translating their config into +`WIREGUARD_PRIVATE_KEY` / `VPN_ENDPOINT_IP` environment variables, the whole +config file is mounted at `/gluetun/wireguard/wg0.conf` and gluetun parses it. +Verified working: gluetun reads the endpoint, interface address and keys +straight out of that file. It also keeps the private key out of `docker inspect` +and out of your shell history, which the environment-variable approach does not. + +**`:latest` on all three images**, which is upstream's habit rather than a +decision. Worth pinning gluetun in particular — it's the one holding the tunnel. + +## Security: the proxies are unauthenticated by default + +Both proxies ship with no credentials, so **anyone who can reach ports 9998 or +9980 can route traffic through your VPN account**. On a trusted LAN that may be +exactly what you want — it's what makes them useful from a browser extension on +another machine. It is not something to expose beyond that, and not something to +forget you did. + +`compose.yaml` has the variables wired already; set both in `.env` and +authentication switches on for both proxies at once: + +```bash +PROXY_USER=someone +PROXY_PASSWORD=a-long-random-string +``` + +Leave them unset and behaviour is unchanged. gluetun needs the user *and* +password together — one alone does nothing. + +Never port-forward these from your router. An open proxy on the public internet +is found within hours, and everything that follows is attributed to your VPN +account. + +## Gotchas + +- **`SOCKSPROXY` is not a gluetun variable.** Neither is `SOCKSPROXY_PORT`. + gluetun ignores both silently — no warning — so the SOCKS5 server never + starts and its published port maps to nothing. Its own settings summary is + the proof: `SOCKS5 proxy server settings: Enabled: no`. The real names are + `SOCKS5_ENABLED=on` and `SOCKS5_LISTENING_ADDRESS=:1080`. Fixed in + `compose.yaml`; still present in `compose.personal.yaml`, whose HTTP proxy + works fine and whose SOCKS port does not. +- **`HTTPPROXY_PORT` is deprecated but does work.** gluetun logs `WARN You are + using the old environment variable HTTPPROXY_PORT, please consider changing + it to HTTPPROXY_LISTENING_ADDRESS`, then brings the proxy up on 8888 anyway. +- **pyload-ng listens on 8000.** The LinuxServer image's web UI port is 8000 + and is *not* settable by environment variable — only inside pyload's own + config in `./pyloadconfig`. So a `8880:8880` mapping reaches nothing unless + you already changed it there; `compose.yaml` publishes `8880:8000` instead. + (Click'n'Load on 9666 isn't published at all.) +- **`wg0.conf` must exist before the first `up`.** It's a bind mount of a + *file*; if the path is missing, Docker creates a **directory** there, gluetun + finds no config, and the error doesn't mention the mount. Same failure mode as + [overleaf's init script](../overleaf/README.md#running-it-on-openmediavault). +- **Never put `wg0.conf` in `init/`.** This repo's `.gitignore` un-ignores + `*/init/**` so that runtime config fragments get committed — which for a + WireGuard config means publishing your private key. `wg*.conf` and `*.ovpn` + are now explicitly ignored everywhere as a guard, but the habit is the real + protection. +- **Restarting gluetun cuts the other two off.** They share its namespace, so + when it goes away their network does too, and `depends_on` doesn't bring them + back. Restart the whole stack, not one service. +- **`TORRENTING_PORT=6881` is not published**, so incoming peer connections + don't arrive and you're limited to outgoing ones. Fixing that properly needs + port forwarding from TorGuard, then publishing the matching port on gluetun. +- **In `compose.personal.yaml` the variables have no defaults.** `${PUID}`, + `${PGID}`, `${TZ}` and `${TEMPDISK}` are bare, so Compose interpolates empty + strings and starts the containers anyway. `compose.yaml` uses + `${PUID:-1000}` and `${TEMPDISK:?…}`, which aborts with a readable message + instead. +- **Check for leaks after any change**, not just at setup. `docker compose exec + gluetun wget -qO- https://ipinfo.io/ip` is the ten-second version. + +## Exposing it + +**Don't.** This stack has no business being reachable from the internet: two +admin UIs with weak-by-default auth and two unauthenticated open proxies. + +If you need the UIs remotely, reach them over your own VPN or a +[Cloudflare Tunnel](../cloudflare-tunnel/) with +[Authentik forward auth](../docs/authentik-sso.md#forward-auth--putting-an-app-with-no-sso-behind-authentik) +in front — and leave the proxy ports off it entirely. + +## Links + +- gluetun (now at the passteque org): +- gluetun wiki: +- TorGuard WireGuard + gluetun guide: + +- qBittorrent image: +- pyload-ng image: diff --git a/gluetun/compose.personal.yaml b/gluetun/compose.personal.yaml new file mode 100644 index 0000000..ae3695b --- /dev/null +++ b/gluetun/compose.personal.yaml @@ -0,0 +1,61 @@ +services: + gluetun: + image: qmcgaw/gluetun + container_name: gluetun + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + environment: + - VPN_SERVICE_PROVIDER=custom + - VPN_TYPE=wireguard + + # Enable SOCKS5 proxy + - SOCKSPROXY=on + - SOCKSPROXY_PORT=1080 + + # Optional HTTP proxy + - HTTPPROXY=on + - HTTPPROXY_PORT=8888 + + ports: + - "9880:9880/tcp" # Qbittorrent Web UI + - "8880:8880/tcp" # PyLoad Web-UI + - "9980:1080/tcp" # Socks Proxy + - "9998:8888/tcp" # HTTP Proxy + volumes: + - /etc/localtime:/etc/localtime:ro + - ./wg0.conf:/gluetun/wireguard/wg0.conf + restart: unless-stopped + + qbittorrent: + image: linuxserver/qbittorrent:latest + container_name: qbittorrent + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + - WEBUI_PORT=9880 + - TORRENTING_PORT=6881 + volumes: + - ./config:/config + - '${TEMPDISK}/downloads/torrents:/downloads' + network_mode: "service:gluetun" + depends_on: + - gluetun + restart: unless-stopped + + pyload-ng: + image: lscr.io/linuxserver/pyload-ng:latest + container_name: pyload-ng + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - ./pyloadconfig:/config + - ${TEMPDISK}/downloads/pyload:/downloads + network_mode: "service:gluetun" + depends_on: + - gluetun + restart: unless-stopped \ No newline at end of file diff --git a/gluetun/compose.yaml b/gluetun/compose.yaml new file mode 100644 index 0000000..bfab91b --- /dev/null +++ b/gluetun/compose.yaml @@ -0,0 +1,95 @@ +# The maintained version of this stack -- this is what `docker compose up` +# uses. See compose.personal.yaml for the older variant still running on +# my own box, and the README for what differs between them. + +services: + gluetun: + # Pinned. This container holds the tunnel; an unattended pull breaking it + # means the download clients lose all connectivity (by design). + image: qmcgaw/gluetun:v3.41.3 + container_name: gluetun + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + + environment: + - VPN_SERVICE_PROVIDER=custom + - VPN_TYPE=wireguard + + # SOCKS5 proxy. The names matter: SOCKSPROXY / SOCKSPROXY_PORT are NOT + # gluetun variables -- it ignores them silently and the proxy never + # starts. Verified against gluetun's own settings summary. + - SOCKS5_ENABLED=on + - SOCKS5_LISTENING_ADDRESS=:1080 + + # HTTP proxy. HTTPPROXY_PORT still works but logs a deprecation warning; + # LISTENING_ADDRESS is the current name. + - HTTPPROXY=on + - HTTPPROXY_LISTENING_ADDRESS=:8888 + + # Proxy authentication. Empty means NO authentication -- anyone who can + # reach the published ports can use the VPN. Set both vars in .env to + # turn it on; leaving them unset keeps the current behaviour. + - HTTPPROXY_USER=${PROXY_USER:-} + - HTTPPROXY_PASSWORD=${PROXY_PASSWORD:-} + - SOCKS5_USER=${PROXY_USER:-} + - SOCKS5_PASSWORD=${PROXY_PASSWORD:-} + + ports: + - "${QBT_PORT:-9880}:9880/tcp" # qBittorrent Web UI + # pyload-ng listens on 8000 inside the container and the port is not + # settable by environment variable -- only in pyload's own config. + - "${PYLOAD_PORT:-8880}:8000/tcp" # pyLoad Web UI + - "${SOCKS_PORT:-9980}:1080/tcp" # SOCKS5 proxy + - "${HTTPPROXY_PORT:-9998}:8888/tcp" # HTTP proxy + # Incoming BitTorrent peers. Needs a forwarded port from TorGuard, and + # the number here must match TORRENTING_PORT below. + #- "6881:6881/tcp" + #- "6881:6881/udp" + + volumes: + - /etc/localtime:/etc/localtime:ro + # Bind mount of a FILE. It must exist before the first `up` or Docker + # creates a directory here and gluetun finds no config. Git-ignored. + - ./wg0.conf:/gluetun/wireguard/wg0.conf:ro + + restart: unless-stopped + + qbittorrent: + image: lscr.io/linuxserver/qbittorrent:latest + container_name: qbittorrent + environment: + - PUID=${PUID:-1000} + - PGID=${PGID:-1000} + - TZ=${TZ:-Etc/UTC} + - WEBUI_PORT=9880 + - TORRENTING_PORT=6881 + volumes: + - ${CONFIG_PATH:-./config}:/config + - "${TEMPDISK:?set TEMPDISK in .env to the disk your downloads live on}/downloads/torrents:/downloads" + # No network of its own: everything goes through the tunnel, and there is + # no route out if the tunnel drops. This is also why it cannot declare + # its own `ports:` -- they live on gluetun above. + network_mode: "service:gluetun" + depends_on: + gluetun: + # gluetun ships a healthcheck; wait for the tunnel rather than racing it. + condition: service_healthy + restart: unless-stopped + + pyload-ng: + image: lscr.io/linuxserver/pyload-ng:latest + container_name: pyload-ng + environment: + - PUID=${PUID:-1000} + - PGID=${PGID:-1000} + - TZ=${TZ:-Etc/UTC} + volumes: + - ${PYLOAD_CONFIG_PATH:-./pyloadconfig}:/config + - ${TEMPDISK}/downloads/pyload:/downloads + network_mode: "service:gluetun" + depends_on: + gluetun: + condition: service_healthy + restart: unless-stopped