Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ USERDIR=/home/username
# roughly 4 minutes. See https://www.plex.tv/claim
PLEX_CLAIM=

# ============ 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=
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_RPC_USERNAME=
TRANSMISSION_RPC_PASSWORD=

# ============ Grafana ============
# Host port to expose Grafana on. Defaults to 3000 if unset.
GRAFANA_PORT=3000
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/compose-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**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.

Expand All @@ -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.
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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_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):

- `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):
Expand Down
26 changes: 21 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,18 +182,34 @@ 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_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_RPC_USERNAME:-}
- TRANSMISSION_RPC_PASSWORD=${TRANSMISSION_RPC_PASSWORD:-}
volumes:
- ${USERDIR}/transmission/config:/config
- ${USERDIR}/transmission/data:/data
restart: unless-stopped

# ============ ERROR MONITORING ============
Expand Down
Loading