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

AppPortRole
Sonarr8989TV
Radarr7878Movies
Lidarr8686Music
Prowlarr9696Indexer manager — syncs indexers to the *arr apps
FlareSolverr8191Solves Cloudflare/JS challenges for some indexers
Bazarr6767Subtitles
qBittorrent8080Torrent download client
Jellyseerr5055Requests (feeds Sonarr/Radarr; talks to Jellyfin/Plex/Emby)
Caddy80/443Optional reverse proxy for http://<app>.home names
gluetunOptional VPN sidecar; routes qBittorrent through WireGuard/OpenVPN
Jellyfin8096Optional 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)

VariableDefaultNotes
PUID / PGID1000UID/GID that owns the media tree
UMASK002group-writable new files (shared media user)
TZEtc/UTCIANA timezone, e.g. Pacific/Auckland
DATA_ROOT./datahost path of the single /data tree
CONFIG_ROOT./configwhere each app stores its config/db
COMPOSE_FILEcolon-separated file list; enables the VPN and/or Jellyfin overlays
JELLYFIN_PUBLISHED_URLURL auto-discovery hands to clients
JELLYFIN_TRANSCODE_TMPFS_SIZE2gsize of the in-RAM transcode scratch area
VPN_SERVICE_PROVIDERmullvadgluetun provider name
VPN_TYPEwireguardwireguard or openvpn
WIREGUARD_PRIVATE_KEY / WIREGUARD_ADDRESSESWireGuard creds
VPN_SERVER_COUNTRIESoptional server pin
LAN_SUBNET192.168.0.0/16lets 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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - abarbarich/arrstack: 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 · GitHub
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

AppPortRole
Sonarr8989TV
Radarr7878Movies
Lidarr8686Music
Prowlarr9696Indexer manager — syncs indexers to the *arr apps
FlareSolverr8191Solves Cloudflare/JS challenges for some indexers
Bazarr6767Subtitles
qBittorrent8080Torrent download client
Jellyseerr5055Requests (feeds Sonarr/Radarr; talks to Jellyfin/Plex/Emby)
Caddy80/443Optional reverse proxy for http://<app>.home names
gluetunOptional VPN sidecar; routes qBittorrent through WireGuard/OpenVPN
Jellyfin8096Optional 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)

VariableDefaultNotes
PUID / PGID1000UID/GID that owns the media tree
UMASK002group-writable new files (shared media user)
TZEtc/UTCIANA timezone, e.g. Pacific/Auckland
DATA_ROOT./datahost path of the single /data tree
CONFIG_ROOT./configwhere each app stores its config/db
COMPOSE_FILEcolon-separated file list; enables the VPN and/or Jellyfin overlays
JELLYFIN_PUBLISHED_URLURL auto-discovery hands to clients
JELLYFIN_TRANSCODE_TMPFS_SIZE2gsize of the in-RAM transcode scratch area
VPN_SERVICE_PROVIDERmullvadgluetun provider name
VPN_TYPEwireguardwireguard or openvpn
WIREGUARD_PRIVATE_KEY / WIREGUARD_ADDRESSESWireGuard creds
VPN_SERVER_COUNTRIESoptional server pin
LAN_SUBNET192.168.0.0/16lets 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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - abarbarich/arrstack: 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 · GitHub
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

AppPortRole
Sonarr8989TV
Radarr7878Movies
Lidarr8686Music
Prowlarr9696Indexer manager — syncs indexers to the *arr apps
FlareSolverr8191Solves Cloudflare/JS challenges for some indexers
Bazarr6767Subtitles
qBittorrent8080Torrent download client
Jellyseerr5055Requests (feeds Sonarr/Radarr; talks to Jellyfin/Plex/Emby)
Caddy80/443Optional reverse proxy for http://<app>.home names
gluetunOptional VPN sidecar; routes qBittorrent through WireGuard/OpenVPN
Jellyfin8096Optional 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)

VariableDefaultNotes
PUID / PGID1000UID/GID that owns the media tree
UMASK002group-writable new files (shared media user)
TZEtc/UTCIANA timezone, e.g. Pacific/Auckland
DATA_ROOT./datahost path of the single /data tree
CONFIG_ROOT./configwhere each app stores its config/db
COMPOSE_FILEcolon-separated file list; enables the VPN and/or Jellyfin overlays
JELLYFIN_PUBLISHED_URLURL auto-discovery hands to clients
JELLYFIN_TRANSCODE_TMPFS_SIZE2gsize of the in-RAM transcode scratch area
VPN_SERVICE_PROVIDERmullvadgluetun provider name
VPN_TYPEwireguardwireguard or openvpn
WIREGUARD_PRIVATE_KEY / WIREGUARD_ADDRESSESWireGuard creds
VPN_SERVER_COUNTRIESoptional server pin
LAN_SUBNET192.168.0.0/16lets 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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - abarbarich/arrstack: 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 · GitHub
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

AppPortRole
Sonarr8989TV
Radarr7878Movies
Lidarr8686Music
Prowlarr9696Indexer manager — syncs indexers to the *arr apps
FlareSolverr8191Solves Cloudflare/JS challenges for some indexers
Bazarr6767Subtitles
qBittorrent8080Torrent download client
Jellyseerr5055Requests (feeds Sonarr/Radarr; talks to Jellyfin/Plex/Emby)
Caddy80/443Optional reverse proxy for http://<app>.home names
gluetunOptional VPN sidecar; routes qBittorrent through WireGuard/OpenVPN
Jellyfin8096Optional 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)

VariableDefaultNotes
PUID / PGID1000UID/GID that owns the media tree
UMASK002group-writable new files (shared media user)
TZEtc/UTCIANA timezone, e.g. Pacific/Auckland
DATA_ROOT./datahost path of the single /data tree
CONFIG_ROOT./configwhere each app stores its config/db
COMPOSE_FILEcolon-separated file list; enables the VPN and/or Jellyfin overlays
JELLYFIN_PUBLISHED_URLURL auto-discovery hands to clients
JELLYFIN_TRANSCODE_TMPFS_SIZE2gsize of the in-RAM transcode scratch area
VPN_SERVICE_PROVIDERmullvadgluetun provider name
VPN_TYPEwireguardwireguard or openvpn
WIREGUARD_PRIVATE_KEY / WIREGUARD_ADDRESSESWireGuard creds
VPN_SERVER_COUNTRIESoptional server pin
LAN_SUBNET192.168.0.0/16lets 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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - abarbarich/arrstack: 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 · GitHub
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

AppPortRole
Sonarr8989TV
Radarr7878Movies
Lidarr8686Music
Prowlarr9696Indexer manager — syncs indexers to the *arr apps
FlareSolverr8191Solves Cloudflare/JS challenges for some indexers
Bazarr6767Subtitles
qBittorrent8080Torrent download client
Jellyseerr5055Requests (feeds Sonarr/Radarr; talks to Jellyfin/Plex/Emby)
Caddy80/443Optional reverse proxy for http://<app>.home names
gluetunOptional VPN sidecar; routes qBittorrent through WireGuard/OpenVPN
Jellyfin8096Optional 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)

VariableDefaultNotes
PUID / PGID1000UID/GID that owns the media tree
UMASK002group-writable new files (shared media user)
TZEtc/UTCIANA timezone, e.g. Pacific/Auckland
DATA_ROOT./datahost path of the single /data tree
CONFIG_ROOT./configwhere each app stores its config/db
COMPOSE_FILEcolon-separated file list; enables the VPN and/or Jellyfin overlays
JELLYFIN_PUBLISHED_URLURL auto-discovery hands to clients
JELLYFIN_TRANSCODE_TMPFS_SIZE2gsize of the in-RAM transcode scratch area
VPN_SERVICE_PROVIDERmullvadgluetun provider name
VPN_TYPEwireguardwireguard or openvpn
WIREGUARD_PRIVATE_KEY / WIREGUARD_ADDRESSESWireGuard creds
VPN_SERVER_COUNTRIESoptional server pin
LAN_SUBNET192.168.0.0/16lets 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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - abarbarich/arrstack: 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 · GitHub
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

AppPortRole
Sonarr8989TV
Radarr7878Movies
Lidarr8686Music
Prowlarr9696Indexer manager — syncs indexers to the *arr apps
FlareSolverr8191Solves Cloudflare/JS challenges for some indexers
Bazarr6767Subtitles
qBittorrent8080Torrent download client
Jellyseerr5055Requests (feeds Sonarr/Radarr; talks to Jellyfin/Plex/Emby)
Caddy80/443Optional reverse proxy for http://<app>.home names
gluetunOptional VPN sidecar; routes qBittorrent through WireGuard/OpenVPN
Jellyfin8096Optional 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)

VariableDefaultNotes
PUID / PGID1000UID/GID that owns the media tree
UMASK002group-writable new files (shared media user)
TZEtc/UTCIANA timezone, e.g. Pacific/Auckland
DATA_ROOT./datahost path of the single /data tree
CONFIG_ROOT./configwhere each app stores its config/db
COMPOSE_FILEcolon-separated file list; enables the VPN and/or Jellyfin overlays
JELLYFIN_PUBLISHED_URLURL auto-discovery hands to clients
JELLYFIN_TRANSCODE_TMPFS_SIZE2gsize of the in-RAM transcode scratch area
VPN_SERVICE_PROVIDERmullvadgluetun provider name
VPN_TYPEwireguardwireguard or openvpn
WIREGUARD_PRIVATE_KEY / WIREGUARD_ADDRESSESWireGuard creds
VPN_SERVER_COUNTRIESoptional server pin
LAN_SUBNET192.168.0.0/16lets 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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - abarbarich/arrstack: 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 · GitHub
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

AppPortRole
Sonarr8989TV
Radarr7878Movies
Lidarr8686Music
Prowlarr9696Indexer manager — syncs indexers to the *arr apps
FlareSolverr8191Solves Cloudflare/JS challenges for some indexers
Bazarr6767Subtitles
qBittorrent8080Torrent download client
Jellyseerr5055Requests (feeds Sonarr/Radarr; talks to Jellyfin/Plex/Emby)
Caddy80/443Optional reverse proxy for http://<app>.home names
gluetunOptional VPN sidecar; routes qBittorrent through WireGuard/OpenVPN
Jellyfin8096Optional 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)

VariableDefaultNotes
PUID / PGID1000UID/GID that owns the media tree
UMASK002group-writable new files (shared media user)
TZEtc/UTCIANA timezone, e.g. Pacific/Auckland
DATA_ROOT./datahost path of the single /data tree
CONFIG_ROOT./configwhere each app stores its config/db
COMPOSE_FILEcolon-separated file list; enables the VPN and/or Jellyfin overlays
JELLYFIN_PUBLISHED_URLURL auto-discovery hands to clients
JELLYFIN_TRANSCODE_TMPFS_SIZE2gsize of the in-RAM transcode scratch area
VPN_SERVICE_PROVIDERmullvadgluetun provider name
VPN_TYPEwireguardwireguard or openvpn
WIREGUARD_PRIVATE_KEY / WIREGUARD_ADDRESSESWireGuard creds
VPN_SERVER_COUNTRIESoptional server pin
LAN_SUBNET192.168.0.0/16lets 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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - abarbarich/arrstack: 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 · GitHub
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

AppPortRole
Sonarr8989TV
Radarr7878Movies
Lidarr8686Music
Prowlarr9696Indexer manager — syncs indexers to the *arr apps
FlareSolverr8191Solves Cloudflare/JS challenges for some indexers
Bazarr6767Subtitles
qBittorrent8080Torrent download client
Jellyseerr5055Requests (feeds Sonarr/Radarr; talks to Jellyfin/Plex/Emby)
Caddy80/443Optional reverse proxy for http://<app>.home names
gluetunOptional VPN sidecar; routes qBittorrent through WireGuard/OpenVPN
Jellyfin8096Optional 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)

VariableDefaultNotes
PUID / PGID1000UID/GID that owns the media tree
UMASK002group-writable new files (shared media user)
TZEtc/UTCIANA timezone, e.g. Pacific/Auckland
DATA_ROOT./datahost path of the single /data tree
CONFIG_ROOT./configwhere each app stores its config/db
COMPOSE_FILEcolon-separated file list; enables the VPN and/or Jellyfin overlays
JELLYFIN_PUBLISHED_URLURL auto-discovery hands to clients
JELLYFIN_TRANSCODE_TMPFS_SIZE2gsize of the in-RAM transcode scratch area
VPN_SERVICE_PROVIDERmullvadgluetun provider name
VPN_TYPEwireguardwireguard or openvpn
WIREGUARD_PRIVATE_KEY / WIREGUARD_ADDRESSESWireGuard creds
VPN_SERVER_COUNTRIESoptional server pin
LAN_SUBNET192.168.0.0/16lets 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