From 013b4a566ba304667e62fb74d2944a88aa049128 Mon Sep 17 00:00:00 2001 From: Josh Cain Date: Wed, 13 May 2026 10:09:44 -0400 Subject: [PATCH 1/2] Switch transmission to haugene/transmission-openvpn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous compose service used linuxserver/transmission, which has no VPN support — despite the README claiming VPN tunneling and .env.example listing OPENVPN_* variables. This PR makes that real. docker-compose.yml: - Swap image to haugene/transmission-openvpn. - Add cap_add NET_ADMIN and devices /dev/net/tun (required by the in-container OpenVPN client). - Pass through OPENVPN_PROVIDER/CONFIG/USERNAME/PASSWORD plus LOCAL_NETWORK (with a 192.168.0.0/16 default so the most common home subnets work out of the box). - Volume changes from /config to /data (haugene's convention covers config + downloads + watch dir under one tree); host path moves from ${USERDIR}/transmission/config to ${USERDIR}/transmission/data. - Pass TRANSMISSION_RPC_USERNAME/PASSWORD optionally (blank = no web UI auth). .env.example: - Generic placeholders for OPENVPN_* (no implied provider endorsement) and a comment pointing at the supported-providers list. - Add LOCAL_NETWORK with a comment explaining why it matters. README.md: - New "Transmission VPN setup" section with required vars, an IP-leak check command, and the most common failure mode (web UI unreachable -> wrong LOCAL_NETWORK). - Add OpenVPN credentials to Prerequisites alongside the other hard-required values. CLAUDE.md: replace the "VPN is aspirational" warning with the new operational facts (volume path, cap_add, LOCAL_NETWORK pitfall). Co-Authored-By: Claude Opus 4.7 --- .env.example | 16 ++++++++++++++++ CLAUDE.md | 2 +- README.md | 36 +++++++++++++++++++++++++++++++++++- docker-compose.yml | 27 ++++++++++++++++++++++----- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index a3af739..b255346 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,22 @@ USERDIR=/home/username # roughly 4 minutes. See https://www.plex.tv/claim PLEX_CLAIM= +# ============ Transmission / OpenVPN ============ +# Transmission runs through haugene/transmission-openvpn and won't start +# without these. See the list of supported providers and config names at: +# https://haugene.github.io/docker-transmission-openvpn/supported-providers/ +OPENVPN_PROVIDER=YOUR_PROVIDER +OPENVPN_CONFIG=your_server_config +OPENVPN_USERNAME=your_vpn_username +OPENVPN_PASSWORD=your_vpn_password +# CIDR(s) of networks that should bypass the VPN tunnel. Must include the +# subnet your machine is on, or the web UI on :9091 will be unreachable. +# Comma-separated for multiple networks (e.g. "192.168.0.0/16,10.0.0.0/8"). +LOCAL_NETWORK=192.168.0.0/16 +# Optional Transmission web UI auth. Leave blank for no auth. +TRANSMISSION_USERNAME= +TRANSMISSION_PASSWORD= + # ============ Grafana ============ # Host port to expose Grafana on. Defaults to 3000 if unset. GRAFANA_PORT=3000 diff --git a/CLAUDE.md b/CLAUDE.md index ec1a9ba..e88861e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,7 +39,7 @@ Radarr and Sonarr are deliberately on both `media_network` (so Seerr, Prowlarr, **No Prometheus in the stack.** There is no `prometheus` service in `docker-compose.yml`. A starting-point config lives at `docs/prometheus.example.yml` for users who want to add Prometheus themselves — don't assume metrics are being scraped today. -**Transmission VPN is aspirational.** The README claims VPN support and `.env.example` has `OPENVPN_*` variables, but the active image is plain `linuxserver/transmission` with no VPN sidecar or `haugene/transmission-openvpn` config. If the user wants real VPN tunneling, that's a change, not a fix. +**Transmission uses `haugene/transmission-openvpn` and won't start without VPN credentials.** The container runs an OpenVPN client internally; `OPENVPN_PROVIDER`, `OPENVPN_CONFIG`, `OPENVPN_USERNAME`, and `OPENVPN_PASSWORD` must all be set in `.env`. The compose service declares `cap_add: NET_ADMIN` and `devices: /dev/net/tun` for the OpenVPN client; the data volume is `/data` (haugene's convention), not `/config` like the linuxserver image. `LOCAL_NETWORK` (CIDR, default `192.168.0.0/16`) controls which destinations bypass the tunnel — if a user reports the web UI is unreachable, this is almost always the cause. **Plex claim tokens expire in ~4 minutes.** `PLEX_CLAIM` must be set in `.env` immediately before `docker compose up -d` on first run. If the user reports a Plex auth issue on first boot, this is almost always why. diff --git a/README.md b/README.md index 58566de..5010d7a 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ A complete, opinionated [Plex Media Server](https://www.plex.tv/) stack delivere - [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) (v2+) - A Plex account and a [claim token](https://www.plex.tv/claim) — generate this **immediately before** your first `docker compose up`; claim tokens expire roughly 4 minutes after they're issued - Values for `DB_PASSWORD`, `JWT_SECRET`, and `COOKIE_SECRET` in `.env` — Tracearr refuses to start without them and will fail the whole stack's `up` command +- OpenVPN credentials from a [supported VPN provider](https://haugene.github.io/docker-transmission-openvpn/supported-providers/) (`OPENVPN_PROVIDER`, `OPENVPN_CONFIG`, `OPENVPN_USERNAME`, `OPENVPN_PASSWORD`) — Transmission tunnels all traffic through OpenVPN and won't start without them. See [Transmission VPN setup](#transmission-vpn-setup) for details ## Getting Started @@ -134,7 +135,7 @@ A ready-to-use [Kometa](https://kometa.wiki/) (Plex Meta Manager) configuration | Service | Description | Port | |---------|-------------|------| -| [Transmission](https://transmissionbt.com/) | Torrent client | `9091` | +| [Transmission (VPN)](https://github.com/haugene/docker-transmission-openvpn) | Torrent client with OpenVPN tunnel — [setup notes](#transmission-vpn-setup) | `9091` | ### Monitoring @@ -176,6 +177,39 @@ Plex runs in host network mode for optimal streaming performance. Radarr and Son Portainer mounts the host's Docker socket (`/var/run/docker.sock`) so it can manage every container. **This grants the Portainer UI root-equivalent access to the host** — anyone who logs in can stop, restart, or exec into any container, including those handling secrets. Set a strong admin password on first launch and don't expose port `9000` to the public internet. +## Transmission VPN setup + +Transmission uses the [`haugene/transmission-openvpn`](https://github.com/haugene/docker-transmission-openvpn) image, which runs an OpenVPN client inside the container and tunnels all torrent traffic through it. The container fails to start without valid VPN credentials. + +**Required `.env` values:** + +| Variable | What it is | +|----------|------------| +| `OPENVPN_PROVIDER` | Provider name from the [supported list](https://haugene.github.io/docker-transmission-openvpn/supported-providers/) (e.g. `MULLVAD`, `PIA`, `NORDVPN`) | +| `OPENVPN_CONFIG` | Server / region config name — provider-specific, see your provider's section in the linked docs | +| `OPENVPN_USERNAME` | VPN account username (the one you use to log into the VPN, not the provider portal) | +| `OPENVPN_PASSWORD` | VPN account password | +| `LOCAL_NETWORK` | CIDR of your LAN (default `192.168.0.0/16`) — traffic to these subnets bypasses the tunnel so the web UI stays reachable | + +**Optional:** + +- `TRANSMISSION_USERNAME` / `TRANSMISSION_PASSWORD` — auth for the Transmission web UI. Leave blank for no auth. + +**Required compose capabilities** (already configured in `docker-compose.yml`, mentioned here in case you fork): + +- `cap_add: [NET_ADMIN]` +- `devices: [/dev/net/tun]` + +**Verifying the tunnel works:** + +```bash +docker compose exec transmission curl -s https://ipinfo.io | grep -E '"(ip|country)"' +``` + +The IP and country in the response should match your VPN exit, not your home connection. If they match your home IP, the tunnel is not active — check `docker compose logs transmission` for OpenVPN errors. + +**If the web UI on `:9091` is unreachable:** `LOCAL_NETWORK` probably doesn't cover the subnet your machine is on. Add your subnet (e.g. `192.168.1.0/24`) to `LOCAL_NETWORK`, comma-separated if you need multiple ranges, and restart the container. + ## Kometa Configuration The `plex-meta-manager/config/` directory contains a ready-to-use [Kometa](https://kometa.wiki/) configuration. Kometa itself is not in `docker-compose.yml` — run it as a one-shot container on whatever schedule you prefer (cron, systemd timer, or a separate compose file): diff --git a/docker-compose.yml b/docker-compose.yml index d42e05f..2736d16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -182,18 +182,35 @@ services: restart: unless-stopped # ============ DOWNLOADING ============ + # haugene/transmission-openvpn tunnels all transmission traffic through an + # OpenVPN connection. NET_ADMIN + /dev/net/tun are required for the + # in-container OpenVPN client. LOCAL_NETWORK must include your LAN CIDR + # or the web UI on port 9091 will be unreachable from your machine. transmission: container_name: transmission - image: linuxserver/transmission - environment: - - TRANSMISSION_RPC_HOST_WHITELIST=192.168.86.* - - TZ=${TZ} + image: haugene/transmission-openvpn + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun networks: - download_network ports: - "9091:9091" + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + - OPENVPN_PROVIDER=${OPENVPN_PROVIDER} + - OPENVPN_CONFIG=${OPENVPN_CONFIG} + - OPENVPN_USERNAME=${OPENVPN_USERNAME} + - OPENVPN_PASSWORD=${OPENVPN_PASSWORD} + - LOCAL_NETWORK=${LOCAL_NETWORK:-192.168.0.0/16} + - TRANSMISSION_RPC_USERNAME=${TRANSMISSION_USERNAME:-} + - TRANSMISSION_RPC_PASSWORD=${TRANSMISSION_PASSWORD:-} volumes: - - ${USERDIR}/transmission/config:/config + - ${USERDIR}/transmission/data:/data + - /etc/localtime:/etc/localtime:ro restart: unless-stopped # ============ ERROR MONITORING ============ From 90676bd6675d706e4526cee25280b774414bc1d8 Mon Sep 17 00:00:00 2001 From: Josh Cain Date: Wed, 13 May 2026 10:50:42 -0400 Subject: [PATCH 2/2] Address CodeRabbit review on transmission VPN PR - compose: switch OPENVPN_PROVIDER/CONFIG/USERNAME/PASSWORD to the fail-fast `${VAR:?must be set}` form so missing credentials are caught at `docker compose up` instead of inside the container. Matches the Tracearr secrets pattern. - compose: drop the /etc/localtime bind mount. TZ=${TZ} handles container timezone and the repo convention is that bind mounts are rooted at ${USERDIR}. - compose + .env.example: rename TRANSMISSION_USERNAME/PASSWORD to TRANSMISSION_RPC_USERNAME/PASSWORD so the .env contract matches the haugene image's env-var contract one-to-one (no in-compose rewriting of var names). - .env.example: OPENVPN_* values are now blank placeholders (paired with the new fail-fast). LOCAL_NETWORK keeps its 192.168.0.0/16 default for the common case. - CI: compose-validate.yml fills in the OPENVPN_* placeholders so the workflow's `cp .env.example .env` step still produces an interpolation-clean .env. - CLAUDE.md: env-var contract section updated to list OPENVPN_* as hard-required (previously contradicted the Transmission paragraph). Removed OPENVPN_* from the "older version" examples. - README: rename the optional auth vars in the VPN setup section. Co-Authored-By: Claude Opus 4.7 --- .env.example | 18 +++++++++--------- .github/workflows/compose-validate.yml | 11 ++++++++--- CLAUDE.md | 6 +++--- README.md | 2 +- docker-compose.yml | 13 ++++++------- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index b255346..27d0979 100644 --- a/.env.example +++ b/.env.example @@ -16,21 +16,21 @@ USERDIR=/home/username # roughly 4 minutes. See https://www.plex.tv/claim PLEX_CLAIM= -# ============ Transmission / OpenVPN ============ -# Transmission runs through haugene/transmission-openvpn and won't start -# without these. See the list of supported providers and config names at: +# ============ Transmission / OpenVPN (required — stack won't start if blank) ============ +# Transmission runs through haugene/transmission-openvpn. See the list of +# supported providers and config names at: # https://haugene.github.io/docker-transmission-openvpn/supported-providers/ -OPENVPN_PROVIDER=YOUR_PROVIDER -OPENVPN_CONFIG=your_server_config -OPENVPN_USERNAME=your_vpn_username -OPENVPN_PASSWORD=your_vpn_password +OPENVPN_PROVIDER= +OPENVPN_CONFIG= +OPENVPN_USERNAME= +OPENVPN_PASSWORD= # CIDR(s) of networks that should bypass the VPN tunnel. Must include the # subnet your machine is on, or the web UI on :9091 will be unreachable. # Comma-separated for multiple networks (e.g. "192.168.0.0/16,10.0.0.0/8"). LOCAL_NETWORK=192.168.0.0/16 # Optional Transmission web UI auth. Leave blank for no auth. -TRANSMISSION_USERNAME= -TRANSMISSION_PASSWORD= +TRANSMISSION_RPC_USERNAME= +TRANSMISSION_RPC_PASSWORD= # ============ Grafana ============ # Host port to expose Grafana on. Defaults to 3000 if unset. diff --git a/.github/workflows/compose-validate.yml b/.github/workflows/compose-validate.yml index 56587dc..b91d693 100644 --- a/.github/workflows/compose-validate.yml +++ b/.github/workflows/compose-validate.yml @@ -22,13 +22,18 @@ jobs: - name: Create .env from example run: | cp .env.example .env - # Tracearr's compose entries use the ${VAR:?must be set} form, so - # blank placeholders in .env.example would fail interpolation. Fill - # them with throwaway values just for the validation step. + # Several compose entries use the ${VAR:?must be set} form (Tracearr + # secrets and OpenVPN credentials), which would fail interpolation + # against the blank placeholders in .env.example. Fill them with + # throwaway values just for the validation step. { echo "DB_PASSWORD=ci-validation" echo "JWT_SECRET=ci-validation" echo "COOKIE_SECRET=ci-validation" + echo "OPENVPN_PROVIDER=ci-validation" + echo "OPENVPN_CONFIG=ci-validation" + echo "OPENVPN_USERNAME=ci-validation" + echo "OPENVPN_PASSWORD=ci-validation" } >> .env - name: docker compose config diff --git a/CLAUDE.md b/CLAUDE.md index e88861e..082238d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ The mounted config directory is `plex-meta-manager/config/`. Its structure is re `.env.example` is the source of truth for what `.env` needs, and every var in it is actually consumed by `docker-compose.yml`. The values fall into two categories: -1. **Hard-required** (stack won't start): `DB_PASSWORD`, `JWT_SECRET`, `COOKIE_SECRET` (all Tracearr — they use the `${VAR:?must be set}` fail-fast form). -2. **Effectively required for the feature to work**: `PUID`/`PGID`/`TZ`/`USERDIR` (everything), `PLEX_CLAIM` (first-boot only), `GRAFANA_PORT` (defaults to 3000 if unset), `PMM_*` (Kometa). +1. **Hard-required** (stack won't start): `DB_PASSWORD`, `JWT_SECRET`, `COOKIE_SECRET` (Tracearr); `OPENVPN_PROVIDER`, `OPENVPN_CONFIG`, `OPENVPN_USERNAME`, `OPENVPN_PASSWORD` (Transmission VPN). All use the `${VAR:?must be set}` fail-fast form. +2. **Effectively required for the feature to work**: `PUID`/`PGID`/`TZ`/`USERDIR` (everything), `PLEX_CLAIM` (first-boot only), `GRAFANA_PORT` (defaults to 3000 if unset), `LOCAL_NETWORK` (Transmission, defaults to `192.168.0.0/16`), `PMM_*` (Kometa), `TRANSMISSION_RPC_USERNAME`/`TRANSMISSION_RPC_PASSWORD` (optional web UI auth). -If a user mentions an env var not in this list (e.g. `OPENVPN_*`, `RADARR_API_KEY`, `DOCKER_INFLUXDB_*`), it's from an older version of the stack — not consumed today. +If a user mentions an env var not in this list (e.g. `RADARR_API_KEY`, `DOCKER_INFLUXDB_*`, `PLEX_TOKEN`), it's from an older version of the stack — not consumed today. diff --git a/README.md b/README.md index 5010d7a..22eb576 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ Transmission uses the [`haugene/transmission-openvpn`](https://github.com/haugen **Optional:** -- `TRANSMISSION_USERNAME` / `TRANSMISSION_PASSWORD` — auth for the Transmission web UI. Leave blank for no auth. +- `TRANSMISSION_RPC_USERNAME` / `TRANSMISSION_RPC_PASSWORD` — auth for the Transmission web UI. Leave blank for no auth. **Required compose capabilities** (already configured in `docker-compose.yml`, mentioned here in case you fork): diff --git a/docker-compose.yml b/docker-compose.yml index 2736d16..fd2f0e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -201,16 +201,15 @@ services: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} - - OPENVPN_PROVIDER=${OPENVPN_PROVIDER} - - OPENVPN_CONFIG=${OPENVPN_CONFIG} - - OPENVPN_USERNAME=${OPENVPN_USERNAME} - - OPENVPN_PASSWORD=${OPENVPN_PASSWORD} + - OPENVPN_PROVIDER=${OPENVPN_PROVIDER:?OPENVPN_PROVIDER must be set} + - OPENVPN_CONFIG=${OPENVPN_CONFIG:?OPENVPN_CONFIG must be set} + - OPENVPN_USERNAME=${OPENVPN_USERNAME:?OPENVPN_USERNAME must be set} + - OPENVPN_PASSWORD=${OPENVPN_PASSWORD:?OPENVPN_PASSWORD must be set} - LOCAL_NETWORK=${LOCAL_NETWORK:-192.168.0.0/16} - - TRANSMISSION_RPC_USERNAME=${TRANSMISSION_USERNAME:-} - - TRANSMISSION_RPC_PASSWORD=${TRANSMISSION_PASSWORD:-} + - TRANSMISSION_RPC_USERNAME=${TRANSMISSION_RPC_USERNAME:-} + - TRANSMISSION_RPC_PASSWORD=${TRANSMISSION_RPC_PASSWORD:-} volumes: - ${USERDIR}/transmission/data:/data - - /etc/localtime:/etc/localtime:ro restart: unless-stopped # ============ ERROR MONITORING ============