Skip to content

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.

Covered — the irreplaceable state:

SourceApp
/volume1/docker-ssd/qbittorrent/appdataqbittorrent
/volume1/docker-ssd/speedtest-tracker/configspeedtest-tracker
/volume1/docker-ssd/upsnap/dataupsnap
/volume1/docker-ssd/qui-bittorrent/quiqui
/volume1/docker-ssd/qbitwebui/dataqbitwebui
/volume1/docker-ssd/anibridge/dataanibridge
volume rustatio_datarustatio
volume arcane-agent-dataarcane-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/Qbittorent and /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 in backup.yaml.
  • What gets paused — the containers carrying the stop-during-backup label.

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.

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-nas

The 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.

Two apps encrypt their on-disk state with a key that lives only in BWS:

AppKeyWhat breaks on rotation
speedtest-trackerSPEEDTEST_TRACKER_APP_KEYEncrypted values in the SQLite DB
qbitwebuiQBITWEBUI_ENCRYPTION_KEYStored 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.

Backups are plain gzipped tarballs — no tooling needed to read them.

Terminal window
# On the NAS
cd /volume1/docker-ssd/infrastructure-toolkit/docker-compose-apps/ugreen-nas
# 1. Inspect before doing anything
tar 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 anything
mkdir -p /volume2/docker-apps-backup/restore-staging
tar 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 it
mv <live-path> <live-path>.pre-restore
# 5. Put the restored data in place, then bring the app back
mv /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:

Terminal window
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.

The data is only half of a restore. You also need:

  1. BWS_ACCESS_TOKENstored 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.
  2. The repo — git clone to /volume1/docker-ssd/infrastructure-toolkit.
  3. The archive, extracted to the original paths.

Then bws run --access-token "$BWS_ACCESS_TOKEN" -- docker compose up -d.

Untested backups fail at the worst possible moment. After the first run:

Terminal window
docker compose logs backup
ls -lh /volume2/docker-apps-backup/
tar tzf /volume2/docker-apps-backup/ugreen-nas-latest.tar.gz | \
awk -F/ '{print $2}' | sort -u

That last command lists the covered services — confirm all eight appear. Worth re-running after adding any app to the stack.