Maintenance¶
This page is for the owner: the day-to-day care of the server. Most of it runs by itself; the
page says what does, what is left to you, and how to check on it. health (the
terminal alias for scripts/stack-health) covers nearly all of it at a glance.
The chores at a glance¶
| Chore | Runs by itself | Left to you |
|---|---|---|
| Image updates | A check every morning at 06:00, pushed to your phone | Apply them with stack-update when it suits you |
| Stack upgrades | Nothing | Pull a new release of this repository when you want its changes |
| Stalled and fake downloads | Cleanuparr, every 5 minutes | Nothing, unless you change its rules |
| Disk space | stack-health fails above 90% full |
Delete what you no longer want |
| Drive health | SMART checks every 6 hours, with a push on failure | Replace a failing drive in time |
| Backups | Every night at 04:30 | Check now and then that they run; keep the password off the server |
| The VPN's forwarded port | vpn-port-sync, every 15 minutes |
Nothing (VPN and ports) |
| Credentials | Nothing | Rotate one when it may have leaked |
Everything that needs a person arrives on your phone through ntfy; the full list of alerts is in Monitoring.
Updates¶
Nothing updates itself. arr-updates.timer runs stack-update-check every morning at 06:00: it
compares each image with what is published for the server's own architecture and pushes anything
new to your phone once, as old -> new, flagging a change of the first version number as
MAJOR (read the release notes of those first). health lists pending updates in yellow.
update # every service with an update
update jellyfin sonarr # or just these
update --rollback jellyfin # previous image and its settings
update --rollback jellyfin --image-only
stack-update snapshots the settings first, keeps each current image as a rollback tag, recreates
only the updated services and runs the health check afterwards. It is never automatic because
these apps depend on each other: a single Jellyfin release once broke the logins of Seerr,
Sonarr, Radarr and Homepage at once. The whole story, including rollback, is in
Updates.
Upgrading the stack¶
stack-update changes app images; the stack itself (the compose file, scripts and units) changes
only when you pull a new version of this repository. Read the release notes on the
releases page first: a major
version lists what to change in .env or on the server, and new keys are in .env.example.
cd ~/homelab-media-stack
sudo scripts/stack-backup pre-update # settings snapshot, kept like update's
git pull --ff-only # main; or git checkout vX.Y.Z for a tagged release
scripts/host-install # re-renders changed units and timers
scripts/stack-up # recreates only the services whose definition changed
health
A release that needs a configure script run again says so in its notes; all of them are safe to re-run.
Cleanuparr¶
Cleanuparr removes downloads that will never finish or should never be imported, and blocklists
them so Radarr, Sonarr and Lidarr grab a different release. Open it at http://<tailscale-ip>:11011,
user admin, password CLEANUPARR_PASSWORD from .env. configure-cleanuparr.py sets it up,
and is safe to re-run.
| Rule | What it does |
|---|---|
| Stalled | No progress at all for about an hour (12 strikes, one per 5-minute run) removes the download, public or private, and blocklists the release. Any progress resets the count, so slow but moving downloads are never touched |
| Failed import | A download that can't be imported after 3 checks is removed and blocklisted |
| Stuck on metadata | A magnet that never resolves is removed after 3 checks |
| Malware | Blocks executables, shortcuts and archives inside downloads |
| Slow downloads | Off: slow is normal on public trackers |
| Download cleaner (seeding, orphans, no-hardlink) | Off: seeding costs no disk space, thanks to hardlinks |
Its safety settings, and why:
- No access to your media. Cleanuparr has no
/datamount; it acts only through the qBittorrent, Radarr, Sonarr and Lidarr APIs and can't touch files itself. - Game downloads are ignored. The qBittorrent category
gamesis on its ignore list: game releases always contain executables and archives, so the malware rule would reject every one. - The malware list is
blacklist_permissive, on purpose. The stricter default list blocks*.srt,*.suband*.idx, so any release carrying its own subtitles would quietly lose them. The permissive list blocks no video, subtitle or audio format. - Archives are blocked, which suits this setup: there is no extractor, so an archived release could never be imported anyway. Blocking it makes the arr apps find a normal one.
- The download cleaner stays off. The configure script refuses to go live if it finds it on.
stack-health confirms all of it: Cleanuparr is healthy, live (not dry-run), its download
cleaner is off, it ignores games, and it has no media mount.
To try a change safely, use dry-run mode: Cleanuparr then only logs what it would do.
scripts/configure-cleanuparr.py --dry-run # dry-run; watch its log in Dozzle
scripts/configure-cleanuparr.py # back to live
scripts/cleanuparr-state # prints e.g. "live downloadcleaner=off ignored=games"
Disk space¶
stack-health fails when the media drive or the system disk is 90% full or more, and Glance's
Lab page shows the media drive's free space, how fast it has grown over the last week, and the
date it would be full at that rate (in red when that is less than 30 days away).
What takes the space, and what to know before freeing it:
- Media and seeding share the space. A finished torrent is hardlinked into the library: the
same file appears in
torrents/andmedia/and uses the disk once. The space comes back only when both are gone: delete the title in Radarr or Sonarr (with its files) and remove its torrent from qBittorrent. Don't delete things out oftorrents/by hand. - Usenet downloads are moved into the library when complete; nothing stays behind.
- Settings backups live on the media drive under
$STORAGE_MOUNT/backups; see Backups for how they are thinned. - On the system disk, Prometheus keeps at most 30 days and 4 GB, container logs are capped at
three 10 MB files per container, and
stack-updatekeeps one rollback image per app.
Drive health¶
Every 6 hours arr-health.timer runs stack-health --notify, which reads the media drive's SMART
data and pushes a failure to your phone. It checks three things:
| Check | Fails when |
|---|---|
| SMART health | The drive's own self-assessment fails |
| Bad sectors | Any reallocated, pending or offline-uncorrectable sector (or, on NVMe, any media error): failure is near |
| Temperature | 55 °C or more |
On a Raspberry Pi it also checks that the board isn't throttled or short of power, and that the chip stays under 80 °C.
For history, Scrutiny (:3006, Dashboards) keeps the SMART
attributes and temperature over time. For a closer look, ask an AI agent with the drive-health
skill, which also checks the USB link of a drive in an enclosure. Before you switch the drive
off, run drive-off first (Storage and boot).
Backups¶
The settings of every app are backed up every night at 04:30; media never is. To check they run:
healthhas a BACKUPS section: the timer is enabled, the last backup is under 48 hours old (checked only while the media drive is mounted), the password copy is on the media drive, and the second copy exists on the system disk.- Glance's Lab page shows when the settings were last backed up, in red after 48 hours.
cat /var/lib/arr-backup/last-successprints the time of the last successful run.
What is backed up, where, the offsite copy and every way to restore: Backups.
Intro Skipper (removed)¶
The Intro Skipper plugin was removed: its audio fingerprinting ran ffmpeg over every new episode
and kept a CPU core busy on a Raspberry Pi. configure-jellyfin-plugins.py uninstalls it and
drops its repository, and stack-health confirms it stays gone. There is no Skip Intro button.
Rotating credentials¶
Every login and key lives in .env (see Configuration). How to
change one depends on who applies it:
| Credential | Change it | Then |
|---|---|---|
DOZZLE_PASSWORD, JELLYDASH_PASSWORD, UPTIME_KUMA_PASSWORD, QUESTARR_PASSWORD, GAMES_PASSWORD, SABNZBD_PASSWORD |
In .env |
Re-run the matching script (configure-dozzle.py, configure-jellydash.py, configure-uptime-kuma.py, configure-questarr.py, configure-sftpgo.py, configure-sabnzbd.py). Each notices the login no longer works and writes the new password |
JELLYFIN_PASSWORD |
In Jellyfin (your user → Password), then in .env |
configure-seerr.py signs in with it |
NAVIDROME_PASS |
In Navidrome, then in .env |
configure-lidarr.py, configure-navidrome.py and viewer-add.py sign in with it |
CLEANUPARR_PASSWORD |
In Cleanuparr, then in .env |
cleanuparr-state and stack-health sign in with it |
GRAFANA_PASSWORD |
In Grafana (your profile), then in .env |
configure-grafana-watch.py signs in with it |
| A viewer's password | Ask them to change it in each app | viewer-add.py never changes an existing account's password |
NTFY_TOPIC |
In .env (the stack's own scripts read it on every run) |
Re-run configure-seerr.py and configure-questarr.py, recreate JellyDash (docker compose up -d jellydash), and change the topic by hand in the ntfy notification of Radarr, Sonarr, Lidarr, Prowlarr and Uptime Kuma (their scripts add it once and leave it alone). Then subscribe to the new topic on your phone |
WIREGUARD_PRIVATE_KEY |
A new Proton configuration | Changes the VPN: see VPN and ports |
RESTIC_PASSWORD |
Not in .env alone |
The existing backup repositories open only with it; see Backups |
An app's API key is copied into several places (other apps, Homepage, Glance, Scraparr). Rotate
one only when it may have leaked, update every app that uses it, and run health afterwards to
find anything still holding the old one.