*The whole arr media stack as one Docker Compose project — with a one-command Proxmox installer.
Sonarr, Radarr, Lidarr, Prowlarr, Bazarr, qBittorrent, FlareSolverr and
Jellyseerr, wired to a single hardlink-friendly /data tree, behind an optional
reverse proxy, with an optional VPN kill-switch for the torrent client. Run it on
a fresh Proxmox LXC with one command, or docker compose up -d on any Docker host.
bash -c "$(curl -fsSL https://raw.githubusercontent.com/abarbarich/arrstack/main/arrstack.sh)"Run it on your Proxmox host as root. It will:
- create a Debian 12 LXC (unprivileged,
nesting=1so Docker works), - optionally format a spare disk as ext4 and bind-mount it as the media store,
- install Docker + the Compose plugin inside the container,
- drop the stack in
/opt/arrstack, generate.env, anddocker compose up -d, - print every app's URL.
With a terminal you get a whiptail menu (resources, network, storage, VPN);
add -y to take defaults from the environment for a fully unattended run.
git clone https://github.com/abarbarich/arrstack.git
cd arrstack
cp .env.example .env # edit PUID/PGID/TZ and DATA_ROOT
docker compose up -d| App | Port | Role |
|---|---|---|
| Sonarr | 8989 | TV |
| Radarr | 7878 | Movies |
| Lidarr | 8686 | Music |
| Prowlarr | 9696 | Indexer manager — syncs indexers to the *arr apps |
| FlareSolverr | 8191 | Solves Cloudflare/JS challenges for some indexers |
| Bazarr | 6767 | Subtitles |
| qBittorrent | 8080 | Torrent download client |
| Jellyseerr | 5055 | Requests (feeds Sonarr/Radarr; talks to Jellyfin/Plex/Emby) |
| Caddy | 80/443 | Optional reverse proxy for http://<app>.home names |
| gluetun | — | Optional VPN sidecar; routes qBittorrent through WireGuard/OpenVPN |
| Jellyfin | 8096 | Optional media server, reading the library off local disk |
All apps are official LinuxServer.io images
(Jellyseerr uses the maintained seerr-team/seerr image). The media server is
opt-in: by default ArrStack feeds an external Jellyfin/Plex/Emby, or you
can run Jellyfin in the stack so it reads the
library from the same disk the *arr apps import to — no network share involved.
Replaces Jackett. Prowlarr is the modern indexer manager and supersedes Jackett for every *arr app, so Jackett is not included.
- Docker Engine + Compose v2 (the Proxmox installer sets these up for you).
- For the Proxmox path: Proxmox VE 8.x (uses
pct/pveam/pvesh). - ~2 GB RAM is plenty (no build steps — everything is a prebuilt image); the Proxmox CT defaults to 4 GB for headroom.
- Storage on one filesystem for the whole
/datatree, so hardlinks work.
Every container sees a single tree so hardlinks and atomic moves work between the download client and the libraries (the TRaSH Guides layout). Copying is avoided entirely — an import is instant and uses no extra space.
/data
├── media/{movies,tv,music} # Sonarr/Radarr/Lidarr libraries
└── torrents/{...,incomplete} # qBittorrent downloads
DATA_ROOT in .env points at the host location of this tree.
scripts/setup-storage.sh (run on the host) formats a blank disk as ext4,
adds it to /etc/fstab by UUID, and bind-mounts it into the CT — with the right
ownership for an unprivileged container (in-CT UID 1000 → host 101000):
# interactive: lists only SAFE, blank disks (hides the OS disk + mounted/LVM/ZFS)
scripts/setup-storage.sh --ctid 111
# non-interactive: format a specific disk
scripts/setup-storage.sh --ctid 111 --disk /dev/sdb -y
# or bind an existing directory instead of formatting anything
scripts/setup-storage.sh --ctid 111 --existing /tank/mediaIt refuses to touch the OS disk or anything mounted / part of LVM/ZFS/RAID, and requires you to type the device path to confirm the (destructive) format.
Route only qBittorrent through a gluetun tunnel. If the VPN drops, gluetun's firewall kills torrent traffic — no leaks — while the rest of the stack keeps working on the LAN.
- Fill the
VPN_*block in.env(provider, WireGuard key, …). - Enable the overlay — either set in
.env:…or pass both files:COMPOSE_FILE=docker-compose.yml:docker-compose.vpn.ymldocker compose -f docker-compose.yml -f docker-compose.vpn.yml up -d(the Proxmox installer does this for you with--vpn). - Verify your exit IP:
docker exec gluetun wget -qO- https://ipinfo.io/ip
Provider-specific values live in the gluetun wiki.
By default there's no media server here. If yours lives on another box and
reaches the library over SMB/NFS, that share is the most fragile part of the
setup — it drops, and playback dies mid-episode. Running Jellyfin in the
stack removes that layer entirely: it reads /data/media from the same local
bind mount the *arr apps write to.
docker compose -f docker-compose.yml -f docker-compose.jellyfin.yml up -d…or set it in .env so plain docker compose up -d picks it up:
COMPOSE_FILE=docker-compose.yml:docker-compose.jellyfin.ymlStacking with the VPN overlay is fine — they touch different services:
COMPOSE_FILE=docker-compose.yml:docker-compose.vpn.yml:docker-compose.jellyfin.ymlThe Proxmox installer offers it too: --jellyfin, or the whiptail prompt.
Then open http://<host-ip>:8096, run the setup wizard, and add libraries
pointing at /data/media/movies, /data/media/tv, /data/media/music.
The library is mounted read-only (Jellyfin keeps its metadata and artwork in
/config), so deleting from the Jellyfin UI won't work — drop the :ro in the
overlay if you want that.
Transcodes are written to a tmpfs at /transcode (RAM, not disk) — set that
path in Dashboard → Playback → Transcode path, and size it with
JELLYFIN_TRANSCODE_TMPFS_SIZE in .env.
Without a GPU, transcoding is CPU-only. Direct play is unaffected — most clients
just play the file — but expect roughly one 1080p transcode on a modest box, and
4K HDR tonemapping is out of reach. To fix that, uncomment the devices: block
in docker-compose.jellyfin.yml:
devices:
- /dev/dri:/dev/drithen enable VAAPI (Intel/AMD) or QSV in Dashboard → Playback.
-
Plain Docker host: that's all. Check the device exists with
ls /dev/dri. -
Proxmox LXC: the host must pass the GPU into the CT first. An unprivileged container shares the iGPU with the host and other guests — unlike a VM, which needs it passed through exclusively. Add to
/etc/pve/lxc/<ctid>.confon the host, then restart the CT:lxc.cgroup2.devices.allow: c 226:* rwm lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dirThe in-CT user also needs to be in the group owning
/dev/dri/renderD128. Checkls -l /dev/driinside the CT: if the render node's GID doesn't match a group yourPUIDbelongs to, addgroup_add: ["<gid>"]to the jellyfin service. (An unprivileged CT shifts UIDs by 100000, so the GID you see inside may differ from the host's.) -
NVIDIA: install the NVIDIA Container Toolkit on the host and use the
deploy.resources.reservations.devicesblock noted in the overlay instead.
Your watch history, users and settings are portable — they're all in Jellyfin's
data directory. Stop the old server, then copy its config, data, metadata
(and optionally cache) directories into ${CONFIG_ROOT}/jellyfin/, chown them
to your PUID:PGID, and start the overlay. Two things to fix afterwards:
- Library paths changed. The old server pointed at something like
/Volumes/media/TV; edit each library to/data/media/tvetc. Jellyfin re-matches by path, so keep the folder split the same and history survives. - Clients point at the old address. Update the server URL in each app, or
set
JELLYFIN_PUBLISHED_URLin.envto the new one.
Starting fresh instead is fine too — you lose watch history, and metadata is re-scraped on first scan.
Jellyseerr's server URL becomes http://jellyfin:8096 (container name, no port
mapping needed — they share the arr network). The Caddyfile already proxies
http://jellyfin.home to the container.
| Variable | Default | Notes |
|---|---|---|
PUID / PGID |
1000 |
UID/GID that owns the media tree |
UMASK |
002 |
group-writable new files (shared media user) |
TZ |
Etc/UTC |
IANA timezone, e.g. Pacific/Auckland |
DATA_ROOT |
./data |
host path of the single /data tree |
CONFIG_ROOT |
./config |
where each app stores its config/db |
COMPOSE_FILE |
— | colon-separated file list; enables the VPN and/or Jellyfin overlays |
JELLYFIN_PUBLISHED_URL |
— | URL auto-discovery hands to clients |
JELLYFIN_TRANSCODE_TMPFS_SIZE |
2g |
size of the in-RAM transcode scratch area |
VPN_SERVICE_PROVIDER |
mullvad |
gluetun provider name |
VPN_TYPE |
wireguard |
wireguard or openvpn |
WIREGUARD_PRIVATE_KEY / WIREGUARD_ADDRESSES |
— | WireGuard creds |
VPN_SERVER_COUNTRIES |
— | optional server pin |
LAN_SUBNET |
192.168.0.0/16 |
lets the LAN reach the WebUI through gluetun |
The stack is running, but the apps don't know about each other yet. The
step-by-step wiring guide with screenshots is in docs/SETUP.md —
Prowlarr indexers + FlareSolverr, syncing indexers to the *arr apps, adding
qBittorrent as the download client, root folders, categories, Bazarr and
Jellyseerr. It's all localhost/container-name wiring inside one stack.
Two things update independently: the apps (Docker images) and ArrStack
itself (the compose files / scripts). On Proxmox, pct enter <ctid> first.
cd /opt/arrstack
docker compose pull # fetch newer images
docker compose up -d # recreate only the containers that changed
docker image prune -f # optional: reclaim old image layersEach app's config/database lives in the CONFIG_ROOT volumes, so nothing is
lost on recreate. With Docker the image is the version — pulling a newer
image is the update, so ignore the *arr apps' built-in "Updates" screen.
The Proxmox one-liner installs /opt/arrstack as a git clone:
cd /opt/arrstack
git pull
docker compose up -dOnly needed when the compose file or scripts change upstream — not day to day. (If you didn't install from a clone, just re-fetch the files or re-run the installer.)
- VPN users: nothing extra —
COMPOSE_FILEin.envmeans everydocker composecommand already includes the gluetun overlay. - Rollback: if a
:latestimage regresses, pin that service to a known-good tag (e.g.image: lscr.io/linuxserver/sonarr:4.0.9) anddocker compose up -d. - Back up first: the config volumes are the only state worth keeping —
tar czf arrstack-config.tgz -C "$CONFIG_ROOT" .before a big jump. - Hands-off auto-updates: add a Watchtower container to pull + recreate on a schedule — trades control for convenience.
These apps ship with no authentication until you set it in each one. Don't expose the container to the internet directly — keep it behind your firewall and reach it over VPN/Tailscale, or front it with an authenticated reverse proxy. Set a WebUI login in each app after first launch.
arrstack.sh # PVE host: create LXC, storage, install Docker, deploy
scripts/setup-storage.sh # PVE host: format ext4 disk + bind-mount into the CT
docker-compose.yml # the stack
docker-compose.vpn.yml # optional gluetun overlay (routes qBittorrent)
docker-compose.jellyfin.yml # optional Jellyfin overlay (media server in-stack)
.env.example # all configuration
caddy/Caddyfile # reverse proxy for *.home names
docs/SETUP.md # the app-wiring walkthrough
lib/helpers.sh # shared logging / helpers for the host scripts
MIT. Provided as-is — review the scripts before running them as root.