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.
This commit is contained in:
@@ -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.
|
||||
@@ -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): <https://github.com/passteque/gluetun>
|
||||
- gluetun wiki: <https://github.com/qdm12/gluetun-wiki>
|
||||
- TorGuard WireGuard + gluetun guide:
|
||||
<https://www.torguard.net/support/articles/TorGuard-Software-Features/torguard-wireguard-gluetun-docker-setup.php>
|
||||
- qBittorrent image: <https://docs.linuxserver.io/images/docker-qbittorrent/>
|
||||
- pyload-ng image: <https://docs.linuxserver.io/images/docker-pyload-ng/>
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user