Backup
Nightly config/state backup for the whole ugreen-nas stack, via
offen/docker-volume-backup.
Archives land in /volume2/docker-apps-backup/ as ugreen-nas-<timestamp>.tar.gz, with
ugreen-nas-latest.tar.gz symlinked to the most recent. Runs at 04:00, prunes after 30
days. All three are overridable — see the backup block in
../../.env.example.
This NAS is not a Proxmox guest, so it sits outside the PBS coverage that protects the rest of the fleet. This job is its only backup.
What is and isn’t covered
Section titled “What is and isn’t covered”Covered — the irreplaceable state:
| Source | App |
|---|---|
/volume1/docker-ssd/qbittorrent/appdata | qbittorrent |
/volume1/docker-ssd/speedtest-tracker/config | speedtest-tracker |
/volume1/docker-ssd/upsnap/data | upsnap |
/volume1/docker-ssd/qui-bittorrent/qui | qui |
/volume1/docker-ssd/qbitwebui/data | qbitwebui |
/volume1/docker-ssd/anibridge/data | anibridge |
volume rustatio_data | rustatio |
volume arcane-agent-data | arcane-agent |
All sources are on /volume1 (pool1) and the archive lands on /volume2 (pool2), so a
single pool failure cannot take both.
Not covered, deliberately:
/volume2/plex/Downloads/Qbittorentand/volume2/metube/*— hundreds of GB of re-acquirable media. Replication territory, not backup territory./home/unknown224/Apps/gluetun— server-list cache, regenerates on start. The last app config still living outside/volume1/docker-ssd.
An app added to the stack needs a matching line in backup.yaml — nothing detects
an uncovered volume for you.
What is not determined by the app compose files
Section titled “What is not determined by the app compose files”The archive contains exactly one directory: BACKUP_SOURCES, which defaults to
/backup. The tool does not discover volumes and has no knowledge of what other
containers mount. Only the paths bound under /backup/ in backup.yaml are archived —
the bulk media that qbittorrent and metube mount is invisible to it.
Two independent lists are easy to conflate:
- What gets archived — the
/backup/*mounts inbackup.yaml. - What gets paused — the containers carrying the
stop-during-backuplabel.
qbittorrent is on the pause list, but only its appdata config directory is on the
archive list. The archive destination is mounted at /archive, deliberately outside
/backup, so backups never nest inside the next backup.
The mount list is the only thing keeping media out. BACKUP_EXCLUDE_REGEXP is present
in backup.yaml but commented out — enable it if the mount list ever grows beyond leaf
config directories, because bind mounts are recursive (rbind) and a share mounted
inside one of the directories above would be pulled in silently. Until then the
archive size is the signal to watch: a sudden jump into GB territory means something got
mounted that shouldn’t have been.
Consistency
Section titled “Consistency”upsnap (PocketBase), speedtest-tracker, and qui keep SQLite databases;
qbittorrent only flushes qBittorrent.conf and BT_backup/ on a clean shutdown.
Copying any of those while the app is writing can produce a file that will not open.
Each is therefore labelled:
labels: - docker-volume-backup.stop-during-backup=ugreen-nasThe scoped value (ugreen-nas rather than the default true) keeps the job from
touching UGOS’s own containers under /volume2/@appstore, which share the same Docker
socket.
gluetun is never stopped. Stopping it orphans the network namespaces of
qbittorrent and rustatio — the failure mode deunhealth exists to recover from.
Stopping those two dependants is safe; they re-attach to the live namespace on start.
Do not add the label to gluetun.
The encryption-key constraint
Section titled “The encryption-key constraint”Two apps encrypt their on-disk state with a key that lives only in BWS:
| App | Key | What breaks on rotation |
|---|---|---|
speedtest-tracker | SPEEDTEST_TRACKER_APP_KEY | Encrypted values in the SQLite DB |
qbitwebui | QBITWEBUI_ENCRYPTION_KEY | Stored qBittorrent credentials |
Rotating either key invalidates every archive taken before the rotation. The restore will appear to succeed — files land, containers start — and the decrypted values will be garbage. There is no error to catch it.
BWS holds only the current value, so the old one is not recoverable after a rotation. If you rotate either key, treat the backup history for that app as gone: take a fresh backup immediately, and don’t expect an older archive to be restorable.
Restoring
Section titled “Restoring”Backups are plain gzipped tarballs — no tooling needed to read them.
# On the NAScd /volume1/docker-ssd/infrastructure-toolkit/docker-compose-apps/ugreen-nas
# 1. Inspect before doing anythingtar tzf /volume2/docker-apps-backup/ugreen-nas-latest.tar.gz | head
# 2. Stop the app being restored (NOT gluetun)docker compose stop <service>
# 3. Extract to a staging path and check it before overwriting anythingmkdir -p /volume2/docker-apps-backup/restore-stagingtar xzf /volume2/docker-apps-backup/<archive>.tar.gz \ -C /volume2/docker-apps-backup/restore-staging backup/<service>
# 4. Move the live data aside rather than deleting itmv <live-path> <live-path>.pre-restore
# 5. Put the restored data in place, then bring the app backmv /volume2/docker-apps-backup/restore-staging/backup/<service> <live-path>Bringing the stack back up must go through bws run — a bare docker compose up -d
starts services with empty credentials:
bws run --access-token "$BWS_ACCESS_TOKEN" -- docker compose up -d <service>Archive paths are prefixed backup/<service>/, matching the mount names in
backup.yaml. Ownership matters on extract: most of these apps run as PUID=1000 with
PGID=10 or 1000 — check the service’s compose fragment and chown -R accordingly if
the app fails to start after a restore.
Full-host rebuild
Section titled “Full-host rebuild”The data is only half of a restore. You also need:
BWS_ACCESS_TOKEN— stored outside this NAS and outside BWS. Without it nothing in the stack can start. This is the one credential that cannot be backed up by this job.- The repo —
git cloneto/volume1/docker-ssd/infrastructure-toolkit. - The archive, extracted to the original paths.
Then bws run --access-token "$BWS_ACCESS_TOKEN" -- docker compose up -d.
Verify it
Section titled “Verify it”Untested backups fail at the worst possible moment. After the first run:
docker compose logs backupls -lh /volume2/docker-apps-backup/tar tzf /volume2/docker-apps-backup/ugreen-nas-latest.tar.gz | \ awk -F/ '{print $2}' | sort -uThat last command lists the covered services — confirm all eight appear. Worth re-running after adding any app to the stack.