Power & Maintenance Runbook¶
How to shut down, physically move, and power back up the homelab with confidence. Audited 2026-10-19 against live state on ghost, ghost-vault, aurora, and the router.
Topology at a glance¶
| Box | Role | Storage | Boot risks |
|---|---|---|---|
| OPNsense @ 192.168.1.1:8443 | router, DHCP, gateway | own appliance | none — designed for reboots; config backup is the gap (see below) |
| pihole3 @ 192.168.1.4 | LAN DNS only (no DHCP) | own box | none; boots independently. While it's off, LAN has no DNS |
| ghost @ 192.168.1.9 | docker host (~126 containers, ~57 compose projects), libvirt (homeassistant VM), SigNoz, traefik, Forgejo, Komodo core, Gotify | nvme root + ZFS pools tank (primary, encrypted, 87% full), harbor (local mirror replica, encrypted), frigate-media |
ZFS key-load → mount → docker ordering is unguarded (below) |
| ghost-vault | offsite-style ZFS replica of ghost + observability forwarders | tank 3.6T |
docker.socket-enabled — comes back fine |
| aurora | desktop, distrobox dev, ollama/stable-diffusion, MCP containers, restic client | ext4/ZFS mix | linger on, docker.socket enabled — comes back; leaked MCP containers are cosmetic |
Data flow (backups)¶
tank/encrypted (ghost, live)
├─ sanoid snapshots (local, ~15min/daily/weekly per template)
├─ syncoid → harbor/encrypted (ghost, same chassis) hourly — ALL children
├─ syncoid → ghost-vault:tank/encrypted hourly — SUBSET (see gaps)
└─ restic daily: aurora /home/nic → sftp:ghost:/tank/encrypted/nas/nic-home
(covers all working repos, .env files, dotfiles — then replicated
onward by the zfs copies above)
Replication health pings healthchecks.io per-leg; failures also push Gotify.
What happens on a power cut + power restore¶
Comes back automatically (verified live)¶
- ghost: ssh.socket ✓, tailscaled ✓, docker (+socket) ✓, libvirtd ✓,
115
unless-stopped+ 11alwayscontainers ✓, ZFS file-keyed datasets mount viazfs-load-key.service(key at/home/nic/.zfs.tank.key) - homeassistant VM — autostart now enabled (was disabled; fixed today)
- ghost-vault: tailscaled ✓, docker.socket ✓, collectors restart
- aurora: linger ✓, tailscaled ✓, docker.socket ✓, user units (collie, devin-dream, nas-backup timer) survive without login
- OPNsense + pihole: independent appliances, self-boot
- Komodo periphery/core are containers — they return with docker
Does NOT come back / needs attention¶
zfs-load-key.servicefails every boot —zfs load-key -ratries thekeylocation=promptchildren ofharbor/encrypted(~12 stale datasets, all unmounted leftovers from the tank migration) and fails non-interactively. File-keyed datasets still load, so it's noisy-not-fatal today — but it means a real key failure would also be ignored, and nothing alerts.- docker↔ZFS ordering gate exists but is toothless. Ghost has
docker-wait-for-zfs.service(Before=docker.service,After=zfs.target) and critical-chain confirms docker waits forzfs.targetat boot. Two defects remain: (a)ExecStart=/bin/true— it never verifies keys loaded or datasets mounted, so a key failure still means docker starts against empty bind-mount dirs (this already happened once — the root-owned skeleton at~/projects/personal/homelab-monoon ghost); (b) the unit hasRequiredBy=docker.servicein[Unit], which systemd ignores (journal: "Unknown key name") — the gate only engages because it'sWantedBy=multi-user.target; a manualsystemctl start dockerbypasses it. Fix below. meme-search-pro— a straydocker runRails container (not in any compose file). restart policy now set; decide if it's keeping.forjeo-db-1— orphan postgres from a typo'd duplicate of forgejo (/var/home/...,restart=always). Verify it's unused, remove.- No deadman's switch for ghost itself. Gotify, SigNoz, traefik all live ON ghost — during a ghost outage nothing can notify you. hc-ping covers replication legs only. Add a "ghost-alive" check (below).
- Vault replication coverage — fixed 2026-10-19. ghost-vault now
receives every docker/* control dataset (forjeo, komodo, karakeep,
repoflow, temporal, signoz — the earlier gap) plus
nas/gamesandnas/nic-home(the restic repo, ~774G). Deliberately excluded (vault is 3.6TiB, keep <75%):nas/media(7.2T replaceable bulk),tdarr_cache, dormant VM disks (ubuntu22-sandbox, win10, talos01, digital-harbor, block-subnet-router, try-cockpit). Exclusions are documented at the top ofzfs-ops/hosts/ghost-vault/syncoid-replication.sh. harborpool has 1 permanent data error (post-resilver; the spare drive was consumed — cron already reminds you Oct 10 to buy one).sudo zpool status -v harborto see the file. It's a replica copy — low urgency, but it is not self-healing.- No ZFS scrub timers enabled —
zfs-scrub-monthly@.timeretc. are disabled; last scrubs were manual (Oct 11–12). Enable monthly scrubs. systemd-networkd-wait-onlinefailed — networking-online ordering is unreliable on ghost; mostly cosmetic but don't depend on it.- Secrets: stack
.envfiles are gitignored. Ghost-side.envs live inside their*-zfsdatasets (covered by harbor; only partially by vault). Aurora-side.envs are in/home/nic→ restic daily. No additional action needed once vault coverage is extended. - OPNsense config backup — export or API-backup the config to the restic path; the router reboots fine, but its config is the thing you'd miss if the box died.
- BIOS power-on-AC-loss — verify on next physical visit for ghost, ghost-vault, router, pihole. Can't check remotely; essential for unattended power cuts.
- Docker on aurora starts via socket activation (
docker.servicedisabled,docker.socketenabled). Works today; one odd boot could leave containers down until something pokes the API.sudo systemctl enable --now docker.serviceremoves the fragility. - Stale cruft:
babyblue/babyblue-auroratailnet entries (offline), ~60 leaked MCP containers on aurora, dead compose projects (minecraft, cannalyze, test-nginx, cloudflared-santa). Cosmetic; clean when bored. - tank at 87% — ZFS degrades past ~90%; plan a cleanup pass
(
nas/dump,games,torrentsare the likely bulk).
Shutdown order¶
Physical move or full power cut:
1. pihole — nothing depends on it mid-move; shutdown whenever
2. ghost — virsh shutdown homeassistant (ACPI; give it ~60s)
optional grace: docker stop $(docker ps -q) — or just
let systemd do it; then shutdown -h now
3. ghost-vault — shutdown -h now (mid-replication is safe; syncoid resumes)
4. aurora — normal desktop shutdown
5. OPNsense — shutdown from UI or power button; goes last so you keep
DHCP/DNS/gateway while the rest go down
Startup order¶
1. OPNsense — wait for web UI + WAN up
2. pihole — DNS back for the LAN
3. ghost — boots itself: zfs import → load-key → mount → docker
4. ghost-vault
5. aurora
Then run the verification script (below) — do NOT eyeball it.
Verification¶
Checks: pool health, key status, mounts, enabled boot units, container restart policies, VM autostart, replication freshness, failed units, disk headroom — on ghost and ghost-vault. Run it before shutdown (baseline) and after power-on; diff the output. Green = walk away.
Fixes to apply (need sudo on ghost / accounts)¶
# Fixes 1–4 (docker-wait-for-zfs hardening, monthly scrubs, harbor -v
# error list, aurora docker.service) are wrapped in a wizard that applies
# them over ssh with one sudo prompt and re-runs the preflight after:
scripts/maintenance/harden-ghost-boot.sh
# 5. External deadman: create an hc-ping check "ghost-alive" (5 min),
# then on ghost add to crontab:
# */5 * * * * curl -fsm5 https://hc-ping.com/<uuid>/ghost-alive
# healthchecks alerts you when it goes quiet — works even when
# gotify/signoz are down with ghost.
# 6. OPNsense config backup: UI → System → Configuration → Backups →
# download; or API GET /api/core/backup/download. Drop the file in
# ~/backups/opnsense (restic picks it up nightly).
# 7. Vault coverage — DONE (2026-10-19, zfs-ops d70ae53). First sends run
# on the hourly timer; ~855G initial transfer, a few hours over LAN.
# Verify later: ssh ghost-vault 'zfs list -r tank/encrypted/docker'
Incident log¶
2026-10-21 — ZFS kernel panic + hard reboot¶
Symptom: All I/O on tank froze mid-afternoon. Load hit ~650; ~15
processes (postgres, apache, minio, both zfs sends) stuck in D state;
pool reported 0 IOPS while disks sat idle with zero hardware errors.
Cause: Kernel logged PANIC: tank: blkptr ... has invalid TYPE 62 —
a write thread hit a corrupted block pointer and wedged inside
zfs_panic_recover. Hardware was clean; this was metadata corruption
already present on the pool (tank had flagged data errors before the
crash). Concurrent sanoid + harbor-sync + vault-pull traffic was the
likely trigger that walked into it, not the cause.
Recovery: sudo reboot. Shutdown stalled on the frozen pool; box
needed a hard power cut. Pool imported cleanly on boot (no panic
replay). signoz-clickhouse crash-looped post-boot: 106 empty broken
parts on signoz_metrics.time_series_v4_6hrs exceeded the
max_suspicious_broken_parts limit (default ~100) so the table refused
to attach. Fix: temporarily raise the limit in
signoz-zfs/config/clickhouse-config.xml (<merge_tree> block is
already there, commented), docker restart, confirm healthy, revert
the config. No sudo needed — that config dir is nic-owned.
Outcome: Full recovery. Preflight 32 PASS / 3 known WARN / 0 FAIL.
Bonus: sanoid's aggressive media template pruned ~3.2T of
snapshot-pinned data post-reboot — tank dropped from 87% to 60%.
Still open:
- tank has 2 data errors, harbor 1 — files unidentified until
sudo zpool status -v <pool> (root-only output; wizard covers it)
- The corrupt metadata still exists — a future write hitting the same
path can panic again. Priority: identify the objects, run a scrub,
restore the damaged files from harbor/vault replicas if real data.
- Consider pausing harbor-replication.timer until status -v is read
if you want zero risk of re-triggering before the damage is known.