Updates¶
This page follows a container image update from the moment a new version is published to the
moment it runs on the server, and back again if it misbehaves. It is for whoever looks after the
server: what checks for updates, what stack-update does step by step, and how to roll back.
Nothing updates itself. A daily check tells you what is new; you decide when to apply it, with one command that takes a settings snapshot first and runs the health check afterwards.
Why not automatic: these apps depend on each other, and one release can break several at once. A single Jellyfin major release once broke logins in Seerr, Sonarr, Radarr and Homepage together. You would rather see that coming. (Watchtower, the usual auto-updater, was also abandoned in December 2025.)
sequenceDiagram
participant T as arr-updates.timer
participant C as stack-update-check
participant R as Image registry
participant N as ntfy
actor You
participant U as update
participant B as stack-backup
participant D as Docker Compose
participant H as stack-health
T->>C: daily at 06:00
C->>R: this architecture's manifest, local vs upstream
C->>C: write the state file and metrics
C->>N: each new update once, MAJOR flagged
N-->>You: push notification
You->>U: update, all or named services
U->>B: sudo stack-backup pre-update
B-->>U: snapshot id, or stop here
U->>D: pull the new images
U->>U: keep the old image as rollback-stamp
U->>D: recreate, qBittorrent and slskd follow gluetun
U->>H: health check, one quiet retry
alt still failing
U->>N: health check failed after update
end
Step 1: the daily check¶
scripts/stack-update-check runs every day at 06:00 from
arr-updates.timer (with up to 30 minutes of random
delay, and at the next boot if the server was off). It only reports: it never pulls or restarts
anything. It does not need the media drive, so it runs while the stack is powered down too.
- It lists the image of every running container (
docker compose ps) and checks six at a time. - For each image it compares this machine's architecture only, not the whole image. Most
images are published for several CPU types under one multi-arch index, and that index changes
whenever any architecture is rebuilt. Comparing indexes would claim an update when only the
amd64 build changed on an arm64 server (the Postgres image did exactly this). So the script
asks the registry which manifest for this architecture (
docker version's server arch) the local index points at, and compares it with the one the tag points at now. - Each image gets one line in
$STATE_ROOT/updates(thestatefolder next toCONFIG_ROOT):current,update <image> <old> <new>, orunknownwhen the registry could not answer. Versions come from the image'sorg.opencontainers.image.versionorbuild_versionlabel. An image labelled onlylatest, or not at all, falls back to its build date (built-2026-09-01), so a notice never reads "latest -> latest". - It writes two Prometheus metrics to
$STATE_ROOT/metrics/updates.prom:arr_image_updates_availableandarr_image_updates_checked_timestamp_seconds. The file is written whole and renamed into place, so a scrape never sees half a file. Glance's Lab page and Grafana read them through Prometheus (see Monitoring). - Updates it has not reported before go to ntfy in one message, one line per service:
media stack: 2 update(s) available
jellyfin: 10.11.2 -> 11.0.0 MAJOR
sonarr: 4.0.15 -> 4.0.16
Apply with: update
MAJOR means the first number of the version changed. Those are the releases that break
things, so read their release notes first. The list of what was already reported lives in
$STATE_ROOT/updates.notified, so each update notifies once.
scripts/stack-health reads the same state file and prints a yellow UPDATES line. It is
information, not a failure:
To check right now, run scripts/stack-update-check (about one to two minutes). It prints each
update available: line and any image it could not check.
Step 2: applying updates¶
Run update (the alias for scripts/stack-update from scripts/aliases.zsh) from the repository:
scripts/stack-update # every service the last check found an update for
scripts/stack-update jellyfin sonarr # or just these
Before you run it:
- Read the release notes of anything marked MAJOR.
- Check that nothing is playing in Jellyfin before updating it; recreating Jellyfin stops every
stream. The
stack-updateskill has a one-line check that reads the API key into a variable. - After a qBittorrent update, expect
scripts/vpn-leak-testto be inconclusive for about a minute: dozens of torrents re-announcing at once briefly swamp the VPN's DNS. It recovers by itself.
What it does, in order:
- Settings snapshot. It runs
sudo scripts/stack-backup pre-update, a normal backup taggedpre-update. If the backup fails, or prints no snapshot id (the media drive is not mounted), it stops and updates nothing. - Notes the current images. It records the image ID each service runs now.
- Pull and recreate.
docker compose pullfor the named services. If the pull fails, nothing was restarted.- For each service whose image actually changed, the old image is tagged
<repository>:rollback-<YYYYmmdd-HHMM>, older rollback tags of that repository are removed (one rollback image per app is enough), and a rollback record goes to$STATE_ROOT/rollback/<service>: image, old image ID, time stamp and snapshot id. Only a changed image replaces the record, so runningstack-updatetwice never leaves the new image as the one to roll back to. docker compose up -drecreates the services. qBittorrent and slskd share gluetun's network namespace and lose it when gluetun is recreated, so when gluetun is in the list they follow it (slskd only when the music module is on). The script then force-recreates them, waits up to three minutes for gluetun to be healthy and runsscripts/vpn-port-sync, because the new tunnel usually brings a new forwarded port (see VPN and ports).- If recreating fails, ntfy gets "media stack: update failed" and the script exits.
- The updated images are marked
currentin the state file and the metrics are rewritten. - Health check. Freshly recreated apps can take a minute to answer, so the first
stack-healthruns quietly. If it fails, the script gives the updated containers up to two minutes to finish starting, waits 30 seconds more, and runs it again with output. If it still fails, ntfy gets "media stack: health check failed after update" with the rollback command, andstack-updateexits non-zero.
After a Jellyfin update, the health check also confirms every Jellyfin plugin is still
Active: a Jellyfin release can disable plugins built for an older one ("NotSupported",
"Malfunctioned") without anything else failing.
stack-update needs passwordless sudo for the snapshot and for restoring settings during a rollback.
Step 3: rolling back¶
flowchart TD
A["An update misbehaves"] --> B{"Could the update have changed the app's database?"}
B -->|"Yes, or not sure"| C["update --rollback svc"]
B -->|"No, certain"| D["update --rollback svc --image-only"]
C --> E{"Rollback record and pre-update snapshot still there?"}
E -->|No| F["Stops, nothing changed. Try --image-only"]
E -->|Yes| G["Restore the app's settings folders to a temporary folder"]
G --> H["Point the tag back at the old image, stop the app"]
H --> I["Delete stale -wal and -shm files, copy over the live folder, fix owner"]
D --> J["Point the tag back at the old image"]
I --> K["Recreate the container"]
J --> K
K --> L{"Was it gluetun?"}
L -->|Yes| M["Recreate qBittorrent and slskd, run vpn-port-sync"]
L -->|No| N["Run stack-health"]
M --> N
scripts/stack-update --rollback jellyfin # previous image and its settings
scripts/stack-update --rollback jellyfin --image-only # previous image, keep current settings
The default restores settings as well as the image. Updates often migrate an app's database,
and the older version cannot open a migrated one. The settings come from the snapshot taken just
before the update, so anything changed in that app since then is lost. Use --image-only only
when you are sure the update did not touch the database.
How a settings rollback works:
- It reads
$STATE_ROOT/rollback/<service>. Without a record (the image did not change in an update) it says so and stops. - It finds the service's settings folders (its volumes under
CONFIG_ROOT) and restores just those from the pre-update snapshot into a temporary folder, using the key file on the media drive. If the restore fails, or the snapshot holds nothing for that app, nothing changes. - It points the image tag back at the old image and stops the app.
- For each folder it deletes the
-waland-shmfiles next to every restored database: the restored databases are complete copies, and stale WAL files beside them would be replayed on top and corrupt them. It then copies the restored files over the live folder rather than swapping it in, because backups leave out caches and artwork, which would otherwise be lost. Ownership is set back to the folder's owner. An empty folder is not in a snapshot, so it is skipped with a note. - It recreates the container. If any folder could not be restored it prints SETTINGS NOT FULLY RESTORED and exits non-zero.
A settings rollback needs the media drive mounted (the snapshots live there). Only the last five
pre-update snapshots are kept, so an old record can point at a snapshot that is gone; the
restore then fails safely and --image-only is the remaining option.
After a rollback the next daily check reports the same update again. Until a fixed release
arrives, name the services you want when you run stack-update, so the bad version is not applied again.
Needle's image¶
Needle's default image, ghcr.io/bugrauluyurt/needle:1 (NEEDLE_IMAGE in .env overrides it),
follows every 1.x release. Minor and patch releases move the :1 tag, so they arrive through the
same daily check and stack-update with no change to the repository. A new major version needs a new
tag in docker-compose.yml, and arrives as a Dependabot pull request. If you set
NEEDLE_IMAGE=needle:local to run your own build, the check cannot compare it with a registry and
lists it under "could not check".
Scripts and units involved¶
| Name | Role | Reference |
|---|---|---|
stack-update-check |
Daily comparison, state file, metrics, ntfy | scripts |
arr-updates.timer / .service |
Runs stack-update-check daily at 06:00; a failure goes to stack-failure-notify |
systemd |
stack-update |
Snapshot, pull, rollback tags, recreate, health check, rollback | scripts |
stack-backup |
The pre-update snapshot |
scripts |
stack-health |
Verifies the result | scripts |
vpn-port-sync |
Re-syncs the forwarded port after gluetun is recreated | scripts |
stack-env.sh |
notify, write_update_metrics, reattach_vpn_apps |
scripts |
stack-update skill |
Lets an AI agent check, apply and roll back, with your yes each time | AI agent |
When it goes wrong¶
stack-updatestops at step 1, saying the backup failed or no snapshot was taken. The media drive is not mounted, or the backup itself failed. Nothing was changed. See Backups.- Radarr, Sonarr or Lidarr say "connection refused" to
gluetun:8080after an update or a rollback of gluetun. qBittorrent and slskd lost gluetun's network.stack-updatere-attaches them, butstack-update --rollback gluetundoes not: rundocker compose up -d --force-recreate qbittorrent slskdandscripts/vpn-port-sync. See qBittorrent stops answering after gluetun restarts. - A Jellyfin plugin shows as not Active. The new Jellyfin disabled a plugin built for the old one. Wait for the plugin's update, or roll Jellyfin back.
- Uptime Kuma reports an updated service down although it runs. See Uptime Kuma says a service is down, but it's running.
- An image is always "could not check". Its registry refused the lookup, or it is a local build with no registry digest.
- For anything else, start with Troubleshooting.