Device letters on merope follow USB enumeration order and are not stable; they changed three times on 2026-08-08/09 as drives were re-plugged. Both documents now address disks by label throughout. Corrections to the migration plan: - The ~28 MiB/s attributed to the media disk was a saturated USB 2.0 link, not a seek-bound spindle. On USB 3 it sustains 183 MB/s, 6.5x faster. Sizing arguments were built on the wrong number. - The 04:42 reset was a UAS device reset on the M.2 bridge (174c:2362), not swap: swap stayed flat at 2.27 GiB with 4.4 GiB available. The watchdog held it to 105s against 48min the previous day. - Both drives now sit on the VL805 SuperSpeed side, so they share one PCIe lane rather than having independent paths. Boot, resolved 2026-08-09 in two stages, both firmware-level and therefore invisible to nixos-rebuild: - EEPROM walks USB in port order and looped when the drive in 2-1 had no bootable partition. Excluded both data drives by VID:PID. Note 'rpi-eeprom-config --apply' is unusable on NixOS, since the wrapper points FIRMWARE_ROOT at a nonexistent path; build the image with --config and stage pieeprom.upd/.sig by hand. - U-Boot 2021.04 predates bootstd and declares USB as a single instance, so boot_targets held one usb0 and only devnum=0 was ever tried. Replaced with 2026.04, which enumerates every bootdev. Old binary kept as u-boot-rpi4.bin.bak. Backups doc records that the M.2 has 2233 corruption errors and has never been scrubbed, so they were found passively rather than by verification. Music (92 GB, the only irreplaceable data there) has been copied to the backups disk; a scrub is now the open item. Assisted-by: pi (claude-opus-5)
9.8 KiB
Backups
There are none. No restic, borg, btrbk or anything else appears anywhere
in this flake, and the 932 GB disk labelled backups, mounted at
/srv/backups, currently holds 5.9 MB.
Device letters are not stable on merope. They follow USB enumeration order and changed on 2026-08-08 when drives were re-plugged. Always address disks by label (
merope,media,backups) or UUID.
The impermanence setup makes this easier than it would otherwise be: every
environment.persistence entry is, by construction, an exact declaration of
what state matters. That list is the starting inventory.
Why this stopped being theoretical
On 2026-08-08, investigating an overnight reset, three things surfaced about the M.2 that holds every service's state:
[merope].corruption_errs 2233
- 2233 checksum failures logged by btrfs on the root filesystem.
- A scrub has never run —
btrfs scrub statusreports "no stats available" against 267 GiB. So those failures were not found by verification; they were found passively, when something read those blocks and got back garbage. Each one returned an I/O error to whatever was reading. - On a single-device btrfs there is no second copy, so that data is gone, and has been for an unknown length of time.
Separately, the USB bridge in front of that disk (ASMedia ASM2362) dropped off the bus entirely at 04:42 and needed a device reset, and on the following boot the SuperSpeed link failed to train at all.
The counter is cumulative and undated — it may all be one bad episode from long ago. But the honest position is that the only copy of firefly, radicale, immich and deluge state currently lives on a disk with confirmed silent corruption, behind a bridge with demonstrated link instability, and nobody has ever checked whether the rest of it reads back.
This moves ahead of merope-disk-reorganize.md. A scrub is the diagnostic
that answers whether the corruption is historical or ongoing — but run it
after there is a copy, not before, because the answer might be "ongoing".
What is actually at risk
Tier 1 — small and irreplaceable (~5 GB)
| what | where | why |
|---|---|---|
| firefly-iii | alcyone /var/lib/firefly-iii |
months of shared expense tracking, hand-entered; sqlite DB and attachments both live here |
| radicale collections | alcyone /var/lib/radicale |
calendars and contacts; khal/khard read from here |
| deluge state | merope /var/lib/deluge |
years of seeding history on a ratio-strict tracker |
alcyone /srv/mail/{vmail,dkim} |
~2.4 GB; replicated to atlas and maia, so this guards against propagated deletion, not disk loss | |
| git repos | alcyone /srv/git |
cgit + git-remote |
*arr + jellyfin DBs |
merope /var/lib/{sonarr,radarr,lidarr,bazarr,prowlarr,jellyfin,jellyseerr} |
rebuildable, but a very long evening |
| headscale | alcyone /var/lib/headscale |
rebuildable, also a long evening |
| host SSH keys | /etc/ssh/ssh_host_* |
without these, sops secrets cannot be decrypted |
Two of these are easy to overlook and rank above the *arr databases:
- radicale holds personal data with no other copy anywhere.
- deluge state — losing it means re-adding every torrent and rechecking ~92 GB, on a tracker that cares about seeding consistency.
Tier 2 — irreplaceable, larger
| what | size | note |
|---|---|---|
| immich | 17 GB | the only category where "irreplaceable" is literal |
| music | 92 GB | private trackers; effectively unobtainable again |
Tier 3 — do not back up
tv and movies, ~4.6 TB, re-acquirable via usenet. The Prometheus TSDB also lands here: historically unrecoverable, but not worth the space.
Budget
tier 1 ~5 GB
immich 17 GB
music 92 GB
------
~115 GB into 932 GB
Roughly eight times headroom, which is comfortable for a year of restic history with the retention below.
The failure that hides until restore day
Copying live database files produces backups that will not open. Nearly everything in tier 1 is a database, and each needs a real dump:
| engine | services | dump |
|---|---|---|
| sqlite | firefly, *arr, jellyfin, jellyseerr |
sqlite3 db ".backup out.db" — safe while running |
| postgres | immich (merope only) | pg_dump |
Roundcube's postgres database is deliberately excluded: it is UI state over IMAP and rebuilds itself. That leaves alcyone needing no database dumps at all — firefly is sqlite, and everything else there is plain files.
Note also that mysql.nix is imported by
hosts/nixos/alcyone/services/default.nix while nothing on that host references
MySQL — a database server running for no one, worth removing separately.
Replication is not backup
Mail and calendars are already replicated across atlas, maia and alcyone, and calendars sync to every client. That covers hardware failure completely and is why they are not urgent.
It covers nothing else. Sync faithfully propagates destruction: one bad
mbsync, one client deciding to resynchronise from empty, one accidental
folder deletion, and all three copies agree on the wrong answer within minutes.
So for replicated data the requirement is not redundancy but time-separated history, and depth matters more than frequency — a propagated deletion may go unnoticed for weeks. These paths are small enough that keeping a year is free.
Back up alcyone's /srv/mail/vmail, not a client's ~/Mail. The server
store is authoritative and covers all three replicas at once; the client copies
are derived from it and one of them being stale or partially synced is exactly
the failure being protected against.
Sizing, from the client replica:
1.7G personal/Archive <- the part that matters
360M personal/Trash
133M personal/Sent
18M personal/Junk
132M usp/Archive
99M usp/Sent
----
~2.4G total
Exclude Junk — it is spam, by definition worthless, and 18 MB × a year of
snapshots for nothing. Trash is a judgement call: keeping it buys a short
safety net for an accidental delete that has not yet been purged, at the cost of
mail you deliberately discarded persisting in backups for twelve months. That is
a privacy question more than a space one.
services.restic.backups.<name>.backupPrepareCommand writes dumps to a staging
directory; backupCleanupCommand removes them afterwards. Back up the dumps,
not the live files.
Immich additionally needs its postgres database and its media files to agree. restic captures both in one run, so the skew is limited to the runtime of a single backup — worst case a handful of assets referencing files that arrive in the next snapshot, not a broken library.
Topology
3-2-1, honestly applied:
alcyone ──restic over tailnet──> merope `backups` disk, /backups
merope ──restic local────────> merope `backups` disk, /backups
merope ──tier 1 + immich─────> offsite object storage (~25 GB)
Backing merope up to a disk inside merope covers the common cases: accidental deletion, a bad rebuild, the M.2 or the media disk failing. It does not cover fire, theft, or the fact that all three disks hang off a single VL805 controller on one PCIe lane. Hence offsite.
One point in its favour: the backups disk reports 0 corruption errors,
against 2233 on the M.2, and it is the only drive still on the USB 2.0 bus — so
it shares neither the SuperSpeed link nor the bridge that has been misbehaving.
480 Mbps is slow for the initial ~115 GB seed (call it two hours) and irrelevant
for incrementals.
Excluding music keeps the offsite set at roughly 25 GB, which is small enough that cost stops being a consideration. Music can be added later if desired; it is the one irreplaceable category that is at least theoretically re-obtainable while tracker access lasts.
Implementation notes
services.restic.backups.<name> covers everything needed declaratively:
paths, exclude, repository, passwordFile (via sops), timerConfig,
pruneOpts, and the prepare/cleanup hooks. Restic encrypts client-side, so an
untrusted offsite target is fine, and deduplicates, so incrementals stay cheap.
Retention to start from:
--keep-daily 7 --keep-weekly 4 --keep-monthly 12
Monthlies are the important part, for firefly and for anything sync-replicated. The realistic failure is not a dead disk; it is noticing in March that something went wrong in January.
The step that gets skipped
A backup that has never been restored is not a backup.
- Quarterly drill: restore firefly's database into a scratch DB and confirm a transaction from months ago actually reads back.
- Wire
restic checkinto the schedule. - Alert on last-successful-backup age. Prometheus and Grafana are already running, and a silently failing backup is worse than none — it stops you worrying about the thing that is no longer happening.
Order of work
- Local repo on sdc1, merope tier 1 only. Smallest useful thing that works.
- Add immich and music.
- Add alcyone over the tailnet.
- Add the offsite target.
- Add monitoring and the restore drill. Not optional; it is the step that turns the rest into a backup rather than a cron job.
Step 1 is worth doing today, in whatever crude form works:
restic -r /srv/backups/restic init
restic -r /srv/backups/restic backup /persist /var/lib
Not declarative, no dumps, no retention — and still strictly better than the current state, which is a disk with 2233 checksum failures and no copy of anything. Replace it with the real thing once it exists.
Note the interaction with merope-disk-reorganize.md: that plan moves /srv to
the media disk and remounts the backups disk at /backups. Paths here assume
the post-migration layout, so either write the repository path so it survives
the move, or accept re-pointing it once. Given the corruption findings, do not
reorder these — backups first, migration second.