Skip to content

Repository files navigation

ArrStack

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


Two ways to install

A) Proxmox VE — one command on the host

bash -c "$(curl -fsSL https://raw.githubusercontent.com/abarbarich/arrstack/main/arrstack.sh)"

Run it on your Proxmox host as root. It will:

  1. create a Debian 12 LXC (unprivileged, nesting=1 so Docker works),
  2. optionally format a spare disk as ext4 and bind-mount it as the media store,
  3. install Docker + the Compose plugin inside the container,
  4. drop the stack in /opt/arrstack, generate .env, and docker compose up -d,
  5. 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.

B) Any Docker host — plain Compose

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

What it installs

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.


Requirements

  • 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 /data tree, so hardlinks work.

Media storage & the /data layout

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.

Formatting + mounting a disk on Proxmox

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/media

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


VPN for qBittorrent (optional)

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.

  1. Fill the VPN_* block in .env (provider, WireGuard key, …).
  2. Enable the overlay — either set in .env:
    COMPOSE_FILE=docker-compose.yml:docker-compose.vpn.yml
    …or pass both files: docker compose -f docker-compose.yml -f docker-compose.vpn.yml up -d (the Proxmox installer does this for you with --vpn).
  3. Verify your exit IP:
    docker exec gluetun wget -qO- https://ipinfo.io/ip

Provider-specific values live in the gluetun wiki.


Jellyfin in the stack (optional)

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

Stacking with the VPN overlay is fine — they touch different services:

COMPOSE_FILE=docker-compose.yml:docker-compose.vpn.yml:docker-compose.jellyfin.yml

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

Hardware transcoding

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/dri

then 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>.conf on the host, then restart the CT:

    lxc.cgroup2.devices.allow: c 226:* rwm
    lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir

    The in-CT user also needs to be in the group owning /dev/dri/renderD128. Check ls -l /dev/dri inside the CT: if the render node's GID doesn't match a group your PUID belongs to, add group_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.devices block noted in the overlay instead.

Migrating an existing Jellyfin

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:

  1. Library paths changed. The old server pointed at something like /Volumes/media/TV; edit each library to /data/media/tv etc. Jellyfin re-matches by path, so keep the folder split the same and history survives.
  2. Clients point at the old address. Update the server URL in each app, or set JELLYFIN_PUBLISHED_URL in .env to the new one.

Starting fresh instead is fine too — you lose watch history, and metadata is re-scraped on first scan.

Telling the rest of the stack about it

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.


Configuration (.env)

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

After install — wire the apps together

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.


Updating

Two things update independently: the apps (Docker images) and ArrStack itself (the compose files / scripts). On Proxmox, pct enter <ctid> first.

Update the apps (the everyday case)

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 layers

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

Update ArrStack itself (only when the repo changes)

The Proxmox one-liner installs /opt/arrstack as a git clone:

cd /opt/arrstack
git pull
docker compose up -d

Only 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.)

Notes

  • VPN users: nothing extra — COMPOSE_FILE in .env means every docker compose command already includes the gluetun overlay.
  • Rollback: if a :latest image regresses, pin that service to a known-good tag (e.g. image: lscr.io/linuxserver/sonarr:4.0.9) and docker 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.

Security

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.


How it works

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

License

MIT. Provided as-is — review the scripts before running them as root.

About

Single-CT Proxmox installer for the *arr media stack (Sonarr/Radarr/Lidarr/Prowlarr/Jackett/Bazarr/Jellyseerr/qBittorrent + optional Jellyfin/Plex) — no Docker, no per-app container sprawl

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages