Declarative Docker Compose for self-hosted media automation, music, and a Jellyfin media server.
Quick start · Profiles · Network isolation · License
Why this exists:
I run self-hosted media services across several machines. Docker Compose profiles let a single compose file serve every host — each machine only starts the services it needs.
What it does:
Provides a declarative docker-compose.yml describing every containerised media service — *arr automation, music/audiobooks, and Jellyfin. Host-specific values are declared in .env.example and never hardcoded.
cd docker-compose-stack/media-stack
cp .env.example .env
# edit .env, set your paths and ports
docker compose --profile arr --profile music --profile jellyfin up -dStart a single profile, or everything at once:
docker compose --profile arr up -d # *arr automation
docker compose --profile music up -d # music + audiobooks
docker compose --profile jellyfin up -d # media server
docker compose --profile arr --profile music --profile jellyfin up -dBring services down with docker compose --profile <name> down.
| Service | Image | Port (host) |
|---|---|---|
| sonarr | hotio/sonarr | 8989 |
| radarr | hotio/radarr | 7878 |
| lidarr | hotio/lidarr | 8686 |
| prowlarr | hotio/prowlarr | 9696 |
| bazarr | hotio/bazarr | 6767 |
| recyclarr | recyclarr/recyclarr | CLI only |
All hotio images run as ${PUID}:${PGID} with ${TZ}. recyclarr is a CLI container with no published ports.
| Service | Image | Port (host) |
|---|---|---|
| music-assistant | music-assistant/server | host network |
| audiobookshelf | advplyr/audiobookshelf | 13378 |
audiobookshelf uses a wget-based healthcheck because its Alpine base image ships no curl.
| Service | Image | Port (host) |
|---|---|---|
| jellyfin | jellyfin/jellyfin | 8096 / 8920 (HTTPS) |
Jellyfin joins the arr network so it can reach sonarr/radarr for metadata lookups. All media volumes are mounted read-only.
Intel QSV hardware transcoding is available but commented out. To enable it, uncomment the devices and group_add lines in the jellyfin service, ensure the render group exists on the host, and add your PUID to it. The host must have an Intel GPU with /dev/dri/renderD128.
Each named network exists so a container can only reach the services it actually talks to:
| Network | Services | Why |
|---|---|---|
arr |
sonarr, radarr, lidarr, prowlarr, bazarr, recyclarr, jellyfin | Genuinely inter-communicating: prowlarr serves indexers, bazarr talks to sonarr/radarr, recyclarr syncs profiles, jellyfin reaches sonarr/radarr for metadata |
music |
audiobookshelf | No real cross-talk; kept on a shared network for consistency |
music-assistant uses network_mode: host and cannot be isolated. A service is only reachable from others that share its network(s).
The arr, music, and jellyfin profiles mount media over NFS from the NAS before any container starts. The compose file expects these mount points on the host, configured via DOWNLOADS_DIR and MEDIA_DIR:
${DOWNLOADS_DIR}(/mnt/downloads)${MEDIA_DIR}/tv,/movies,/anime,/cartoons,/music,/audiobooks
If the NFS share is unavailable, the containers start but the mount paths are empty. Mount the shares first, then run docker compose up.
These run as systemd/native processes and are intentionally not part of this file:
- Jellyseerr (request manager)
- qBittorrent (download client)
- Transcoder
- Reverse proxy (NPM)
The compose stack references some of these as upstreams (for example, sonarr/radarr connect to qBittorrent at ${QBITTORRENT_HOST}).
All configurable values are declared in .env.example. Copy it to .env and set your own values. API keys are not stored in either file — export them in your shell before docker compose up:
export SONARR_API_KEY=... RADARR_API_KEY=...This stack assumes a reverse proxy (nginx, etc.) exists elsewhere for TLS. There is no TLS terminator in this file — the services publish plain HTTP to the host LAN. Point your reverse proxy at the published ports and terminate TLS there.
MIT © 2026 JD Cordero