Skip to content

Configuration reference

Every setting in .env and every compose profile (module): what it does, whether you must set it, its default, and what reads it. It is for anyone filling in .env or switching modules on and off; Getting started walks through a first setup.

How .env is read

.env lives in the repository root, holds every secret and every value specific to your server, and is gitignored. Start from .env.example, which lists every key with a one-line hint, and keep it private (chmod 600 .env).

  • Docker Compose substitutes ${KEY} in docker-compose.yml from it, and also reads COMPOSE_PROFILES and COMPOSE_FILE from it, so plain docker compose commands in the repository follow them.
  • Bash scripts source it through stack-env.sh, which exports every key. Python scripts parse it through stack_env.py.
  • Quoting: wrap a value that contains $ in single quotes (Compose and bash would expand it), and a value with spaces too, for example WEATHER_LOCATION='<city>, <country>'. The Python scripts strip the quotes, so every tool reads the same value.
  • Keys the scripts write: SABNZBD_API_KEY (configure-sabnzbd.py), JELLYSTAT_API_KEY and CHANGEDETECTION_API_KEY (configure-glance.py), JELLYDASH_JELLYFIN_API_KEY (configure-jellydash.py) and YOUTUBE_REFRESH_TOKEN (youtube-sync.py --login). Leave them empty and run the script.
  • Changing a value that a container reads takes effect when the container is recreated (docker compose up -d <service>); a value a configure script applies takes effect when you re-run that script.

In the tables below, Required is yes when the core stack or a core script fails without the key, the module's name when only that module needs it, no for an optional feature, and filled by a script for the keys above. Default is what is used when the key is empty or missing; none means there is no fallback (the example file's value, where it has one, is given in brackets). Read by names containers (compose services) and scripts. Each key's anchor is its name in lower case, for example #storage_mount.

Keys

This machine

Key Required Default What it does Read by
STACK_USER yes none Linux user that owns the repository and runs the stack's services and timers. host-install (systemd templates), stack-backup, stack-health
HOST_NAME yes none The server's host name, also its Tailscale MagicDNS name (hostname). Used for links on the dashboards and as an allowed host. homepage, glance, changedetection, grafana; vpn-port-sync (qBittorrent's allowed hosts), stack-backup (restic host), stack-health (<host>.local resolves to a LAN address)
TAILSCALE_IP yes none The server's Tailscale IPv4 address (tailscale ip -4). configure-jellyfin-plugins.py (external links), stack-health (SFTPGo's admin refused on it)
STORAGE_MOUNT no /mnt/storage Where the media disk is mounted. See below. host-install, stack-up, drive-off, stack-health, stack-update, stack-backup, configure-uptime-kuma.py, glance, the skills
STORAGE_UUID no empty Filesystem UUID (lsblk -f) of a media drive you switch on by hand, so it mounts on power-on. Leave empty when the disk is always mounted through fstab. host-install (99-arr-storage.rules)
STORAGE_DEVICE no /dev/sda The media drive's whole-disk device, for Scrutiny's SMART history. See below. scrutiny
QUESTARR_ALLOWED_ORIGINS games none Every URL Questarr is opened on (host name, <host>.local, LAN IP, Tailscale IP, localhost), each with port 5000, comma-separated. questarr

Identity and permissions

Key Required Default What it does Read by
PUID yes none (1000) User ID the containers run as; must own DATA_ROOT (id). the linuxserver images and every service with user:; configure-navidrome.py
PGID yes none (1000) Group ID the containers run as. same as PUID
UMASK yes none (002) 002 makes new files group-writable, so every container can import the others' files. the linuxserver images, slskd
TZ yes none (Etc/UTC) Your time zone (timedatectl list-timezones). every container that takes TZ; glance
WEATHER_LOCATION dashboards empty Location of Glance's weather widget; single-quote it when it has spaces. glance

Paths

Key Required Default What it does Read by
DATA_ROOT yes none One folder on the media mount holding both downloads and media, so hardlinks work. Don't split it; stack-health fails when it is not on STORAGE_MOUNT. every container that mounts /data or the media; configure-navidrome.py, configure-sftpgo.py, stack-health
CONFIG_ROOT yes none App databases, settings and API keys, deliberately outside the repository. The scripts keep their state in the state folder beside it. every container's config volume; every script through stack-env.sh and stack_env.py

Network and modules

Key Required Default What it does Read by
LAN_CIDR yes none Your home network's range. gluetun (reachable outside the tunnel), host-firewall, stack-health, configure-sabnzbd.py, configure-plex.py
ARR_SUBNET no 172.18.0.0/16 The subnet of the arr Docker network. Docker gives 172.18.0.0/16 to the first other compose project on a machine, so host-install checks for an overlap and names a free range; move PROWLARR_IP into the new subnet with it. docker-compose.yml, host-install
PROWLARR_IP yes none (172.18.0.200) Prowlarr's fixed address on the arr Docker network (inside ARR_SUBNET). Questarr trusts Prowlarr's download links only when their host matches the address it syncs from. prowlarr, configure-questarr.py
QBIT_PORT yes none (8080) qBittorrent's web UI port, published by gluetun; every app and script reaches qBittorrent on it. gluetun, qbittorrent, homepage, glance; stack-env.sh, vpn-port-sync, stack-health, downloads-throttle, configure-arr.py
COMPOSE_PROFILES no * for the scripts Which optional modules run: * for all, or a list such as music,monitoring. See compose profiles. Docker Compose; stack-env.sh, stack_env.py and every script that skips a module
MEDIA_CPUS no 3 CPU cores Jellyfin, Plex and Byparr may each use. See below. jellyfin, plex, byparr
COMPOSE_FILE no docker-compose.yml Commented out in the example. Add compose.gpu.yml for hardware transcoding or compose.vpn-failover.yml for a verified Proton server pool, separated by colons. Both overrides can be combined. See below and VPN failover. Docker Compose
HWACCEL no empty Hardware transcoding type: vaapi (Intel or AMD) or qsv (Intel). Commented out in the example. configure-jellyfin-plugins.py

Proton VPN

Key Required Default What it does Read by
WIREGUARD_PRIVATE_KEY yes none The PrivateKey of a dedicated Proton WireGuard configuration (a P2P server, NAT-PMP on, Moderate NAT off). Its endpoint configuration is installed separately; see VPN and ports. The pre-commit hook blocks committing it. gluetun
VPN_COUNTRIES yes none Comma-separated countries the VPN may exit in, spelled as gluetun logs them. stack-health fails when the exit country isn't listed or is the United States. Custom mode uses the endpoint in your WireGuard configuration. The failover preset also passes this as gluetun's SERVER_COUNTRIES filter and refuses an empty value. gluetun in failover mode, stack-health, the vpn-check skill
VPN_SERVER_NAMES with failover preset empty Comma-separated server names printed by vpn-import-servers. Limits failover to those imported servers; the preset refuses an empty value. Ignored in custom mode. gluetun in failover mode

Plex

Key Required Default What it does Read by
PLEX_CLAIM plex, first run only empty A claim token from plex.tv/claim (it expires in about 4 minutes). Can be blank afterwards. plex

Recyclarr

Key Required Default What it does Read by
RADARR_API_KEY yes none Radarr's API key (Radarr, Settings, General). Most scripts fall back to Radarr's config.xml when it is empty; the containers and configure-jellyfin-plugins.py can't. recyclarr, homepage, glance, scraparr (through configure-scraparr); stack-health and every script that talks to Radarr
SONARR_API_KEY yes none Sonarr's API key, the same way. recyclarr, homepage, glance, scraparr; stack-health and every script that talks to Sonarr

Jellyfin admin

Key Required Default What it does Read by
JELLYFIN_USER no admin Jellyfin admin created by the startup wizard; Seerr signs in to Jellyfin with it. configure-jellyfin.py, configure-seerr.py
JELLYFIN_PASSWORD yes none Its password. same
JELLYFIN_SERVER_NAME no Home Media The server name apps show. configure-jellyfin-plugins.py

Dashboard and monitoring

Key Required Default What it does Read by
HOMEPAGE_ALLOWED_HOSTS yes none Every host:port Homepage is opened on; Homepage refuses other Host headers. The scripts reuse the host names for SABnzbd's allowed host names and Jellyfin Enhanced's Seerr links. homepage, configure-sabnzbd.py, configure-jellyfin-plugins.py
PROWLARR_API_KEY yes none Prowlarr's API key (Settings, General). The scripts fall back to Prowlarr's config.xml. homepage, glance, scraparr; stack-health and every script that talks to Prowlarr
BAZARR_API_KEY dashboards, monitoring none Bazarr's API key. configure-bazarr.py reads Bazarr's own config file instead. homepage, glance, scraparr
SEERR_API_KEY yes none Seerr's API key. homepage, glance, jellydash; viewer-add.py, activity-watch, configure-jellyfin-plugins.py
JELLYFIN_API_KEY yes none An API key you create in Jellyfin (Dashboard, API Keys) after its startup wizard. homepage, glance; configure-arr.py, configure-jellyfin-plugins.py, configure-jellydash.py, viewer-add.py, activity-watch, downloads-throttle, stack-health
GRAFANA_USER monitoring none (admin) Grafana's admin. grafana, configure-grafana-watch.py
GRAFANA_PASSWORD monitoring none Its password. same
JELLYSTAT_DB_USER stats none (jellystat) Postgres user of Jellystat's database. jellystat-db, jellystat, stack-backup (pg_dump)
JELLYSTAT_DB_PASSWORD stats none Its password. jellystat-db, jellystat
JELLYSTAT_JWT_SECRET stats none Jellystat's JWT secret; any random string. jellystat
JELLYSTAT_JELLYFIN_API_KEY no empty A Jellyfin API key dedicated to Jellystat, which you paste into Jellystat's own setup page. Nothing in the stack reads it; .env just keeps it. nothing

Glance

Key Required Default What it does Read by
JELLYSTAT_API_KEY filled by a script empty A Jellystat key named glance, created by configure-glance.py. glance
CHANGEDETECTION_API_KEY filled by a script empty The key ChangeDetection.io generates on first start, copied by configure-glance.py. glance

YouTube (Glance)

Key Required Default What it does Read by
YOUTUBE_CLIENT_ID no empty A Google OAuth client of type "TVs and Limited Input devices" with the YouTube Data API enabled, for Glance's video rows. youtube-sync.py
YOUTUBE_CLIENT_SECRET no empty Its secret. youtube-sync.py
YOUTUBE_REFRESH_TOKEN filled by a script empty Stored by youtube-sync.py --login. Without it the pinned channel lists stay and videos come from YouTube's unreliable RSS feed. youtube-sync.py, stack-health

Needle and Spotify

Key Required Default What it does Read by
SPOTIFY_CLIENT_ID no empty A Spotify app (developer.spotify.com, needs Premium) for Spotify in Needle. Its redirect URI is <NEEDLE_PUBLIC_URL>/api/spotify/callback. needle
SPOTIFY_CLIENT_SECRET no empty Its secret. needle
NEEDLE_PUBLIC_URL music empty Needle's Tailscale Serve address, https://<host>.<tailnet>.ts.net:4535. needle, homepage, glance
NEEDLE_IMAGE no ghcr.io/bugrauluyurt/needle:1 Needle's image. See below. needle

Backups

Key Required Default What it does Read by
RESTIC_PASSWORD yes none Encrypts the restic backups. stack-backup also keeps a copy beside the repository on the media drive. stack-backup; stack-update and stack-health use the copy on the drive
RESTIC_OFFSITE_REPO no empty An offsite restic repository (an s3: or b2: URL, for example) that receives a copy of every daily backup. Its credentials go into .env too, as the backend's own variables (AWS_*, B2_*, AZURE_*, GOOGLE_* or RCLONE_*), which stack-backup passes to restic. stack-backup

Cleanuparr

Key Required Default What it does Read by
CLEANUPARR_PASSWORD yes none Cleanuparr's web login (user admin). configure-cleanuparr.py, cleanuparr-state (so stack-health)

Questarr

Key Required Default What it does Read by
QUESTARR_PASSWORD games none Questarr's web login (user admin); any password, it is written as a hash. configure-questarr.py
IGDB_CLIENT_ID no empty Game search and covers in Questarr, from your own Twitch developer app (dev.twitch.tv/console). configure-questarr.py
IGDB_CLIENT_SECRET no empty Its secret. configure-questarr.py

Music

Key Required Default What it does Read by
SOULSEEK_USER music none Soulseek account name. slskd registers it on first login, so pick any unique name. slskd
SOULSEEK_PASS music none Its password; a random string. slskd
SLSKD_PASSWORD music none slskd's web login (user admin). slskd
SLSKD_API_KEY music none The API key Lidarr and Needle use for slskd; a random string. slskd, needle; configure-lidarr.py, stack-health
NAVIDROME_USER music none (admin) Navidrome's admin, created by configure-lidarr.py. configure-lidarr.py, configure-navidrome.py, viewer-add.py
NAVIDROME_PASS music none Its password. same
LIDARR_API_KEY music none Lidarr's API key (Settings, General). The scripts fall back to Lidarr's config.xml. homepage, glance, needle; stack-health and every script that talks to Lidarr

Notifications

Key Required Default What it does Read by
NTFY_SERVER no https://ntfy.sh The ntfy server for push notifications. jellydash; every script's notify, configure-arr.py, configure-seerr.py, configure-questarr.py, configure-uptime-kuma.py
NTFY_TOPIC no empty The ntfy topic. Anyone who knows the name can read it, so make it long and random. Empty means no pushes, and configure-arr.py, configure-seerr.py, configure-questarr.py and configure-uptime-kuma.py skip their ntfy steps. same

Uptime Kuma

Key Required Default What it does Read by
UPTIME_KUMA_USER monitoring none (admin) Uptime Kuma's admin. configure-uptime-kuma.py
UPTIME_KUMA_PASSWORD monitoring none Its password; any password works, it is written as a hash. configure-uptime-kuma.py

Dozzle and JellyDash

Key Required Default What it does Read by
DOZZLE_USER monitoring none (admin) Dozzle's login. configure-dozzle.py
DOZZLE_PASSWORD monitoring none Its password. configure-dozzle.py
JELLYDASH_USER stats none (admin) JellyDash's admin. jellydash, configure-jellydash.py
JELLYDASH_PASSWORD stats none Its password; JellyDash's 8-character minimum doesn't apply, it is written as a hash. configure-jellydash.py
JELLYDASH_JELLYFIN_API_KEY filled by a script empty JellyDash's own Jellyfin API key, created by configure-jellydash.py. jellydash

Games download page (SFTPGo)

Key Required Default What it does Read by
GAMES_USER games none (admin) Your login to the games download page. Every account is read-only, so yours grants nothing a viewer's doesn't. configure-sftpgo.py
GAMES_PASSWORD games none Its password. configure-sftpgo.py
SFTPGO_ADMIN_USER games none (gamesadmin) SFTPGo's admin, created on first start and used only by the scripts through the REST API; it may log in only from the server and Docker. sftpgo, games_accounts.py, stack-health
SFTPGO_ADMIN_PASSWORD games none Its password; use a long random value (openssl rand -base64 24). same

Usenet

Key Required Default What it does Read by
USENET_HOST no empty Your Usenet provider's server. When set, configure-arr.py adds SABnzbd to the arr apps. configure-sabnzbd.py, configure-arr.py
USENET_PORT no 563 Its SSL port. configure-sabnzbd.py
USENET_USER with USENET_HOST none Provider login. configure-sabnzbd.py
USENET_PASS with USENET_HOST none Provider password; single-quote it when it contains $. configure-sabnzbd.py
USENET_CONNECTIONS no 20 Connections SABnzbd opens. configure-sabnzbd.py
NZBGEEK_API_KEY no empty Adds the NZBgeek indexer to Prowlarr. configure-arr.py
NZBFINDER_API_KEY no empty Adds the NZBFinder indexer to Prowlarr; it is strong on French releases. configure-arr.py
DRAUPNIRR_API_KEY no empty Adds the semi-private draupnirr tracker (French TV) to Prowlarr. configure-indexers.py
TR4KER_API_KEY no empty Adds the semi-private tr4ker tracker (French TV) to Prowlarr. configure-indexers.py
SABNZBD_USER no empty (admin) SABnzbd's web login; the apps use its API key instead. configure-sabnzbd.py
SABNZBD_PASSWORD no empty Its password. configure-sabnzbd.py
SABNZBD_API_KEY filled by a script empty SABnzbd's API key, copied by configure-sabnzbd.py. homepage, glance; configure-questarr.py, configure-jellydash.py

Subtitle providers (Bazarr and Jellyfin)

Each provider is enabled only when its keys are filled. Re-run configure-bazarr.py after changing them, and configure-jellyfin-plugins.py after changing OPENSUBTITLES_USER or OPENSUBTITLES_PASS. Both Open Subtitles credentials are needed for Jellyfin's plugin; Bazarr and Jellyfin share the account's download limits.

Key Required Default What it does Read by
OPENSUBTITLES_USER no empty opensubtitles.com account for Bazarr and Jellyfin's official Open Subtitles plugin. configure-bazarr.py, configure-jellyfin-plugins.py
OPENSUBTITLES_PASS no empty Its password, shared by Bazarr and Jellyfin. configure-bazarr.py, configure-jellyfin-plugins.py
SUBSOURCE_API_KEY no empty SubSource API key (subsource.net, profile). configure-bazarr.py

Jellyfin Enhanced extras

Re-run configure-jellyfin-plugins.py after changing these.

Key Required Default What it does Read by
TMDB_API_KEY no empty themoviedb.org v3 API key: reviews and "where to stream". configure-jellyfin-plugins.py
MDBLIST_API_KEY no empty mdblist.com API key: IMDb, Rotten Tomatoes and Metacritic ratings on posters. configure-jellyfin-plugins.py

Compose profiles

The stack is a core plus optional modules, each a Docker Compose profile. COMPOSE_PROFILES in .env picks the modules.

Module Services Skipped when the module is off
core (always on) gluetun, qbittorrent, sabnzbd, prowlarr, radarr, sonarr, bazarr, seerr, recyclarr, byparr, jellyfin, cleanuparr, socket-proxy nothing
music slskd, lidarr, navidrome, needle configure-lidarr.py, configure-navidrome.py, the Lidarr steps of configure-arr.py, the Navidrome and Needle steps of viewer-add.py
books shelfmark, calibre-web-automated Manual first-run setup; see books.
games questarr, sftpgo configure-questarr.py, configure-sftpgo.py, the games login in viewer-add.py
dashboards homepage, glance, changedetection configure-glance.py, youtube-sync.py (so arr-youtube.timer does nothing)
monitoring prometheus, node-exporter, scraparr, grafana, dozzle, uptime-kuma, scrutiny configure-dozzle.py, configure-uptime-kuma.py, configure-grafana-watch.py, configure-scraparr
stats jellystat, jellystat-db, jellydash configure-jellydash.py, configure-grafana-watch.py, the Jellystat key in configure-glance.py, the Jellystat dump in stack-backup
plex plex configure-plex.py

stack-health skips the checks of services that are off, stack-up requires only the library services of the modules that are on, and configure-uptime-kuma.py monitors only enabled services.

* or a list

  • COMPOSE_PROFILES=* turns every module on. This is the example file's value.
  • A comma-separated list, such as COMPOSE_PROFILES=music,monitoring, turns on only those modules; the core always runs.
  • Empty or missing, the scripts treat it as * (they export that default to every docker compose call they make), but a docker compose command you type yourself sees no profile and handles only the core. Set it explicitly.

Switching a module off

  1. Remove the module from COMPOSE_PROFILES in .env.
  2. Stop and remove its containers yourself: stack-up's docker compose up -d --remove-orphans leaves them running, because their services are still defined in docker-compose.yml. For example, for the games module: docker compose rm -sf questarr sftpgo.
  3. Re-run configure-uptime-kuma.py if monitoring is on, so it deletes the module's monitors.

From then on the scripts skip the module (they print ~ <service> is off (COMPOSE_PROFILES in .env); skipped). The dashboards don't follow COMPOSE_PROFILES: Homepage's apps/homepage/services.yaml and Glance's apps/glance/glance.yml keep every card, and a switched-off module's cards show an error or no data. Remove them from those files if they bother you.

Module Homepage cards Glance widgets
music Needle, Lidarr Lab page: the Music entry of Apps. Media page: Coming up in music
games Questarr, Game downloads Lab page: the Questarr and Games page entries of Apps
monitoring Uptime Kuma, Dozzle, Scrutiny, Grafana Lab page: Media drive (Scrutiny and Prometheus), Uptime, Maintenance (Prometheus), and the movie and TV sizes in Library (Prometheus)
stats JellyDash, Jellystat Media page: Most watched this month, Watching last 30 days
plex Plex none
dashboards the whole of Homepage the whole of Glance, including Watched pages (ChangeDetection.io)

Hardware and resources

compose.gpu.yml and COMPOSE_FILE

compose.gpu.yml is an override that gives Jellyfin and Plex the host's /dev/dri, for hardware transcoding on Intel or AMD graphics on an x86-64 machine. A Raspberry Pi 5 has no video encoder, so leave it off there. The linuxserver images join /dev/dri's group themselves.

  1. In .env, set COMPOSE_FILE=docker-compose.yml:compose.gpu.yml and HWACCEL=vaapi (Intel or AMD) or HWACCEL=qsv (Intel).
  2. Recreate Jellyfin so it gets the device: docker compose up -d jellyfin (and plex when that module is on).
  3. Run configure-jellyfin-plugins.py. With HWACCEL set, it sets Jellyfin's hardware acceleration to that type with the device /dev/dri/renderD128, turns on hardware encoding and 10-bit HEVC decoding, and decodes H.264, HEVC and VP9 in hardware.

Docker Compose reads COMPOSE_FILE from .env, so every docker compose command in the repository, the scripts' included, uses both files. No script changes Plex's transcoder settings. activity-watch alerts only on software transcodes, so hardware ones stay quiet.

When using the VPN failover preset too, preserve it with COMPOSE_FILE=docker-compose.yml:compose.gpu.yml:compose.vpn-failover.yml.

CPU limit

The cpus: limit of jellyfin, plex and byparr, each on its own; default 3. Keep it below the machine's core count so a transcode or Byparr's headless browser can't take every core.

STORAGE_MOUNT and STORAGE_DEVICE

STORAGE_MOUNT (default /mnt/storage) must be a real mount point: arr-stack.service and stack-up refuse to start without it, so media never lands on the system disk, and stack-health fails when DATA_ROOT is not on it. On a single-disk machine, bind-mount a folder there, for example with the fstab line /srv/media /mnt/storage none bind 0 0. The installed units embed the mount path and its mount unit name, so re-run host-install after changing it. Storage and boot covers the drive's power cycle.

STORAGE_DEVICE (default /dev/sda) is the whole-disk device Scrutiny reads SMART data from; it appears as /dev/sda inside the container. A /dev/disk/by-id/... path survives device reordering (ls -l /dev/disk/by-id/). apps/scrutiny/collector.yaml sets the device type sat, for a drive behind a USB-SATA bridge, and the container gets SYS_RAWIO so smartctl can reach it. stack-health doesn't use this key: it finds the disk from the mount and adds -d sat itself for a USB drive.

Needle image

Needle's image; the default ghcr.io/bugrauluyurt/needle:1 follows every 1.x release. To run your own build, build it from a Needle checkout with docker build -t needle:local <needle checkout>, set NEEDLE_IMAGE=needle:local, and recreate Needle (docker compose up -d needle).