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.
.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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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-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.
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.
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.
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)
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.
In .env, set COMPOSE_FILE=docker-compose.yml:compose.gpu.yml and HWACCEL=vaapi (Intel or AMD) or HWACCEL=qsv (Intel).
Recreate Jellyfin so it gets the device: docker compose up -d jellyfin (and plex when that module is on).
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.
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 (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'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).