A complete, opinionated Plex Media Server stack delivered as a single Docker Compose file — bootstrap streaming, request handling, library automation, downloads, and monitoring with one command.
- One command, full stack — Plex, request handling (Seerr), library automation (Radarr/Sonarr), downloads, monitoring, and metadata curation all wired together.
docker compose up -dand you're done. - Pre-built Kometa config included — IMDb Top 250 / Trakt / streaming-service collections, daily rotating playlists, and resolution/HDR overlays are ready to run, not a blank YAML you fill in over weeks.
- Network-isolated by design — four separate Docker networks split streaming, request flow, downloading, and monitoring so a misbehaving service can't talk to the rest.
- Stream analytics in the box — Tracearr ships built-in for concurrent-stream monitoring, geolocation, and account-sharing detection alongside Tautulli's usage reporting.
- Docker and Docker Compose (v2+)
- A Plex account and a claim token — 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, andCOOKIE_SECRETin.env— Tracearr refuses to start without them and will fail the whole stack'supcommand - OpenVPN credentials from a supported VPN provider (
OPENVPN_PROVIDER,OPENVPN_CONFIG,OPENVPN_USERNAME,OPENVPN_PASSWORD) — Transmission tunnels all traffic through OpenVPN and won't start without them. See Transmission VPN setup for details
-
Clone the repository:
git clone https://github.com/joshdev8/AutoPlexx.git cd AutoPlexx -
Copy
.env.exampleto.envand fill in your values:cp .env.example .env
-
Set
USERDIRin.envto the parent directory where configs and media should live. All services bind-mount under${USERDIR}/<service>, so this one variable controls where everything lands — you don't normally need to editdocker-compose.ymlitself. -
Pre-create Seerr's config directory and give it to your
PUID:PGID. Load the values from.envinto your shell first so the paths expand correctly (or substitute the literal values from your.env):set -a; source .env; set +a mkdir -p "${USERDIR}/docker/seerr/config" sudo chown -R "${PUID}:${PGID}" "${USERDIR}/docker/seerr/config"
Unlike the
linuxserverimages (Radarr, Sonarr, etc.), the Seerr container runs as a fixed non-rootnodeuser and does not honorPUID/PGID— it can't fix ownership itself. If this directory doesn't already exist, Docker creates it asrooton first boot and Seerr crashes withEACCES: permission denied, mkdir '/app/config/logs/'. Creating it up front owned by your user avoids that. -
Start all services:
docker compose up -d
-
Open the dashboard at http://localhost:8090 (or whatever
DASHBOARD_PORTyou set —8090is the default).There is nothing to configure — it shows every container's live status straight away, and links out to each service's own UI. As the rest of the stack finishes its first boot, the dashboard picks up each service's API key from the config file that service writes and lights up the corresponding panels on its own. See Dashboard for details.
docker compose ps # show running services
docker compose logs -f <service> # tail one service's logs
docker compose restart <service> # restart one service after editing its config
docker compose pull && docker compose up -d # update images (Watchtower also does this on a schedule)
docker compose down # stop everything (named volumes preserved)
docker compose config # validate YAML + env interpolation without starting anythingflowchart LR
User([User])
Watchlist[Plex Watchlist]
User -->|streams from| Plex
User -->|requests via| Seerr
Watchlist -->|synced by| Watchlistarr
Seerr -->|submits to| Radarr
Seerr -->|submits to| Sonarr
Watchlistarr -->|drives| Radarr
Watchlistarr -->|drives| Sonarr
Prowlarr -->|provides indexers to| Radarr
Prowlarr -->|provides indexers to| Sonarr
Radarr -->|sends torrents| Transmission
Sonarr -->|sends torrents| Transmission
Transmission -->|completed files| Plex
Bazarr -->|fetches subtitles for| Plex
Plex -->|stream activity| Tautulli
Plex -->|stream activity| Tracearr
Tracearr --> TimescaleDB[(TimescaleDB)]
Tracearr --> Redis[(Redis)]
Telegraf -->|host + container metrics| Grafana
Tautulli -->|usage data| Grafana
cAdvisor -->|container metrics| Prometheus
NodeExporter[node-exporter] -->|host metrics| Prometheus
Prometheus -->|scraped time-series| Grafana
See Network Architecture below for the exact network membership of each service.
Screenshots of the running stack — Plex, Seerr, Tautulli, Grafana, and Tracearr — coming soon. Contributions welcome via PR.
| Service | Description | Port |
|---|---|---|
| Plex | Central media server | 32400 (host network) |
A ready-to-use Kometa (Plex Meta Manager) configuration is included for automated collections and overlays, but Kometa itself is not part of docker-compose.yml — see Kometa Configuration for how to run it.
| Service | Description | Port |
|---|---|---|
| Seerr | Content request and management interface | 5055 |
| Radarr | Movie management and downloading | 7878 |
| Sonarr | TV show management and downloading | 8989 |
| Prowlarr | Indexer manager that feeds Radarr/Sonarr | 9696 |
| Bazarr | Subtitle management for Radarr/Sonarr libraries | 6767 |
| FlareSolverr | Cloudflare bypass proxy for Prowlarr indexers | 8191 |
| Maintainerr | Rule-based Plex library cleanup (auto-remove watched/aged items) | 6246 |
| Watchlistarr | Syncs Plex watchlist to Radarr/Sonarr | N/A |
| Decluttarr | Removes stalled / failed downloads from *arr queues | N/A |
| Checkrr | Scans media files for codec / corruption issues | 8585 |
| Cleanarr | Finds and removes duplicate content | N/A |
| Requestrr | Discord bot for content requests | 4545 |
| Service | Description | Port |
|---|---|---|
| Transmission (VPN) | Torrent client with OpenVPN tunnel — setup notes | 9091 |
| Service | Description | Port |
|---|---|---|
| Tautulli | Plex usage monitoring | 8181 |
| Grafana | Metrics visualization | 3000 |
| Prometheus | Time-series metrics database — scrapes cAdvisor + node-exporter | 9090 |
| cAdvisor | Per-container CPU / memory / network metrics | 8080 |
| node-exporter | Host (CPU / disk / network) metrics | 9100 |
| Telegraf | Metrics collection agent | N/A |
| Tracearr | Stream tracking and account sharing detection | 3001 |
| Portainer | Docker management UI (note on socket access) | 9000 |
| AutoPlexx Dashboard | Unified status and launcher for the whole stack — details | 8090 (default, set by DASHBOARD_PORT) |
| Service | Description | Port |
|---|---|---|
| Watchtower | Automated container updates | N/A |
| TimescaleDB | Time-series database (used by Tracearr) | N/A (internal) |
| Redis | Cache/queue (used by Tracearr) | N/A (internal) |
| docker-socket-proxy | Read-only Docker API for the dashboard — why | N/A (internal) |
These services pair well with this stack but are not included in the default docker-compose.yml. See their respective docs to add them:
- Lidarr - Music management and downloading
- Jackett - Torrent indexer aggregator (Prowlarr covers most use cases)
Services are isolated into separate Docker networks:
monitoring_network- Tautulli, Grafana, Telegraf, Watchtower, Portainer, Prometheus, cAdvisor, node-exporter, Dashboard, docker-socket-proxymedia_network- Seerr, Radarr, Sonarr, Prowlarr, Bazarr, FlareSolverr, Maintainerr, Checkrr, Dashboarddownload_network- Transmission, Watchlistarr, Cleanarr, Requestrr, Decluttarr, Radarr, Sonarr, Dashboardtracearr-network- Tracearr, TimescaleDB, Redis
Plex runs in host network mode for optimal streaming performance. Radarr and Sonarr are attached to both media_network (so Seerr, Prowlarr, and Bazarr can reach them) and download_network (so Watchlistarr and Transmission can reach them). The dashboard joins all three service networks because it aggregates from all of them, and reaches Plex through host.docker.internal.
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.
The AutoPlexx Command Center — at http://localhost:8090 by default, or whichever port DASHBOARD_PORT names — is a single pane over the whole stack: live container status for every service, and a launcher that opens each one's own UI.
It works out of the box, with no required configuration. DASHBOARD_PORT (default 8090) sets the published port; everything else the dashboard reads is optional and has a working default — see dashboard/README.md for the full list, including the *_API_KEY overrides you only need if you run a service outside this stack.
API keys are not something you paste in. Each service writes its own key to a config file — Sonarr, Radarr, Prowlarr and Bazarr to config.xml, Tautulli to config.ini, Seerr to settings.json — and the dashboard reads those files through the read-only /discover mounts declared in docker-compose.yml. Discovery re-runs while the dashboard is running, so on a first boot each panel lights up by itself shortly after the service behind it comes up. No restart, no wizard.
If you run one of these services outside this stack, set the matching *_API_KEY variable in .env and it takes precedence over discovery.
Container status has to come from the Docker API, but the dashboard does not mount the Docker socket. Note that :ro on a socket only affects the file node — it does not make the Docker API read-only — so a direct mount would be a second root-equivalent exposure alongside Portainer's.
Instead, docker-socket-proxy sits in front with CONTAINERS=1 and everything else off. It permits GET /containers/json and denies the rest, including image access and exec. The dashboard talks HTTP to that proxy and never sees the socket.
The dashboard is read-only — it issues no mutating calls to any service — and it ships no authentication. Treat it as LAN-only.
By default Compose publishes it on all interfaces (0.0.0.0), which is what makes http://<your-server>:8090 work from another machine on your LAN. If you put it behind a reverse proxy for authentication, publishing on all interfaces lets anyone reach the dashboard directly and skip that proxy entirely. Bind it to loopback so only the proxy can reach it:
# in .env
DASHBOARD_BIND=127.0.0.1Then point your reverse proxy at 127.0.0.1:8090. If the proxy runs in Docker rather than on the host, drop the ports: mapping for dashboard altogether and let the proxy reach it over a shared Docker network instead — a published port always bypasses the proxy. Restricting 8090 at the firewall works too, but the bind address is the harder thing to get wrong.
The same reasoning applies to every other service in this stack, none of which are authenticated by default either.
docker compose up -d pulls a prebuilt image, so no Node toolchain is needed. To build from source instead:
docker compose build dashboard && docker compose up -d dashboardTo work on it locally, see dashboard/README.md.
Transmission uses the haugene/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 (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:
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.
The plex-meta-manager/config/ directory contains a ready-to-use Kometa 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):
docker run --rm \
-v "$(pwd)/plex-meta-manager/config:/config" \
--env-file .env \
kometateam/kometaThe bundled config drives:
- Movies - IMDb Top 250, TMDb trending, Trakt lists, Oscar categories, genre collections, streaming service collections, holiday collections, and more
- TV Shows - Popular/trending, streaming networks, genres, studios (Marvel, DC), year-based collections
- Playlists - Daily rotating playlists by genre (Action, Comedy, Animated, Family)
- Overlays - Resolution badges (4K, 1080p, 720p), HDR/Dolby Vision, IMDb Top 250, streaming service badges, show status (Airing/Ended/Canceled)
