Problem: node and cross-cutting narratives lived at the repo root
(containers/, vms/, infrastructure/, host .md files), interleaved with the
machine-readable substrate.
Change:
- Move containers/ -> knowledge/wiki/containers/, vms/ -> knowledge/wiki/vms/,
infrastructure/ -> knowledge/wiki/infrastructure/, hosts/{hubris,strong}.md ->
knowledge/wiki/hosts/, infrastructure/references/ -> knowledge/sources/references/,
GLOSSARY.md -> knowledge/GLOSSARY.md.
- Add knowledge/{index.md,log.md,sources/index.md} scaffolding.
- Rewrite all relative links repo-wide via a path-resolving mapper (inbound +
outbound + between-moved-files), including .hermes/, runbooks, operations,
investigations, plans, README, AGENTS.
- Repoint inventory.yaml doc_page fields and regenerate hosts/*.yaml (which
embed doc_page); update oikos/gen-topology.py output path, candidate doc
paths, and footer links; update code-comment doc paths.
Substrate untouched in place: inventory.yaml, hosts/*.yaml (regenerated,
idempotent), oikos/ code, mcp/, secrets/, bin/.
Verification:
- Logical broken-link set identical to pre-move baseline (net 128 -> 127; the
topology regen fixed one, introduced none). Remaining are pre-existing refs
to destroyed/archived nodes, out of scope for this move.
- gen-topology.py --check exit 0 (in sync); cards carry knowledge/wiki/ doc paths.
- build_host_files.py idempotent; all inventory doc_page targets resolve.
- MCP contract verified: get_page/search_docs/get_changelog resolve moved pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
132 — rclone
Off-host backup appliance. Mirrors selected /mnt/library folders to Proton Drive
with a plain rclone sync (monthly), and serves rclone's Web GUI on the LAN for browsing
and ad-hoc runs. Replaces the disabled restic-on-USB job — see backups.
Provisioned 2026-07-01. (LXC 131 was already taken by an undocumented teddycloud container,
so this landed on 132.)
At a glance
- Hostname:
rclone - IP:
192.168.8.214(static, set in PVEnet0config — same pattern as grimmory/authentik) - Privilege: privileged (root in-container = host root → reads every
/mnt/librarysubtree, incl.homecloud/anddocuments/, regardless of owner) - Resources: 1 core / 2 GiB RAM / 8 GiB rootfs (Debian 13) — bumped from 1 GiB on 2026-07-03
after
rclone-rcd.servicewas OOM-killed under real load (see "Known issue" below) - Mounts:
/mnt/libraryread-only (mp0: /mnt/library,mp=/mnt/library,ro=1) — a backup job must never be able to write into the library - Public hostname: none — the UI is LAN-only, no auth (by design)
Service / port map
| Service | Listen | Notes |
|---|---|---|
rclone Web GUI (rcd) |
192.168.8.214:5572 |
rclone-rcd.service, --rc-no-auth, LAN-only. Browse /mnt/library + proton:, run ad-hoc syncs, live job status |
| monthly mirror | — | rclone-backup.service + .timer (OnCalendar=*-*-01 03:00) |
Backup design
- Mode: plain mirror —
rclone sync(Proton mirrors local; deletions propagate; no versioning). - Encryption: Proton Drive's built-in E2E only (no rclone
cryptoverlay → files stay browsable in Proton's web UI). - Selected set:
/etc/rclone-backup/folders.list— one absolute source path per line (#/blank ignored). This file is the picked set the monthly timer mirrors. Extensible to other disks once bind-mounted into this LXC. - Path mapping: source
S→proton:library-backup/<S without leading slash>(e.g./mnt/library/notes→proton:library-backup/mnt/library/notes). - Runner:
/usr/local/sbin/rclone-backup.sh [folder ...](Python, despite the.shname — kept the path stable) — no arg = every enabled line. Submits each folder as an async job through the rclone rc API served byrclone-rcd.service(the same daemon backing the Web GUI on:5572), so scheduled/ad-hoc runs show up live in the GUI's Jobs panel, not just in logs. Gentle on Proton's rate limits (Transfers=4, TPSLimit=8, FastList=truevia the rc_configpayload). The rc API here requires POST for every call includingjob/statusandcore/stats— GET with query params 404s. - Logs / "past runs": per-run logs in
/var/log/rclone-backup/<safe>-<ts>.log; one-line JSON summary per run appended to/var/log/rclone-backup/runs.jsonl. - Failure notify:
OnFailure=rclone-backup-notify@%n.service→ logs to journal today; TODO wire to Hermessend_message(Matrix) per backups.
rclone + Proton Drive
- rclone installed from the official binary (not apt) so the
protondrivebackend is present (rclone v1.74.3). - Remote
proton:(typeprotondrive). Config at/root/.config/rclone/rclone.conf, mode 600. This file is a secret (holds the obscured Proton password + TOTP secret + session) — never commit it. Escrow the Proton account creds in the password manager. - Config gotchas (from rclone docs/forum):
- Log into Proton via a browser at least once first, or key generation fails.
- For unattended runs, store the TOTP secret (not a 6-digit code) so rclone self-generates
codes; obscure with
rclone obscure. - Passwords with extended-ASCII characters are known to break auth.
- Proton's API is rate-limited → keep
--transfers/--tpslimitconservative (baked into the runner).
- DR escrow (pending): store the Proton creds as sops secret
secrets/protondrive.yaml, granted to this LXC's age key, so the remote can be rebuilt after a re-provision.
The UI (rclone Web GUI)
rclone rcd --rc-web-gui --rc-no-auth --rc-addr 0.0.0.0:5572 (assets auto-downloaded on first
start). Reach it at http://192.168.8.214:5572 on the LAN.
Security note:
--rc-no-authexposes full rclone control — including deleting remote data — to anyone on the LAN (accepted per the design choice). The container has only a LAN NIC, so it is not publicly reachable. Harden later by adding--rc-user/--rc-passor fronting it with Authentik.
Tracked config (deferred)
Not yet tracked. The runner, systemd units, and folders.list currently live as plain files
directly on the LXC — fully functional, just not version-controlled or auto-deployed. A
dtoro/rclone gitea repo package (runner, units, install.sh, webhook receiver) is pre-built and
staged at /root/rclone-repo on the LXC for whenever this gets tracked (Shape A, like
caddy). Gitea ALLOWED_HOST_LIST already includes 192.168.8.214 in anticipation.
See auto-deploy.
Selected folders (live in /etc/rclone-backup/folders.list): /mnt/library/cloud (287G),
/mnt/library/documents (249M), /mnt/library/repos (83M). /mnt/library/notes was synced once as
a connectivity test (not in the recurring set). Proton quota checked: 2 TiB plan, ~1.65 TiB free
after this set.
Enrollment gotcha: pct exec PATH
pct exec (lxc-attach) does not source /etc/environment or run a login shell, so
/usr/local/bin (where bootstrap installs sops) isn't on $PATH by default — bootstrap's own
command -v sops post-install check failed under pct exec even though the binary installed fine.
Fixed by symlinking /usr/local/bin/{sops,homelab} into /usr/bin (always on the minimal PATH),
rather than relying on /etc/environment. Same category as the documented pct exec no-initgroups
gotcha — worth adding to
agent-enrollment.md troubleshooting if it recurs
on future LXC bootstraps.
Known issue: rclone-rcd.service OOM-killed under 1 GiB RAM (root cause, resolved)
What looked like repeated "protondrive silently stalls" was actually rclone-rcd.service (the rc
API daemon backing the Web GUI and, since the rc-API redesign, all actual sync work) getting
OOM-killed under the original 1 GiB RAM allocation — journalctl confirms
A process of this unit has been killed by the OOM killer at the exact moment a transfer had
"frozen." systemd's own Restart=on-failure (5s) auto-respawns it, but every in-flight job's state
is lost on the kill, which looked identical to a silent backend hang from the outside (frozen
core/stats, no new log lines). Fix: bumped the LXC's memory to 2 GiB (pct set 132 -memory 2048 — applies live via the host cgroup, confirmed via cat /sys/fs/cgroup/lxc/132/memory.max on
hubris, no container reboot needed). After the bump, the full folder set (cloud 287G, documents
249M, repos 83M) completed cleanly with no further kills.
rclone-backup-watchdog.timer (every 5 min) → rclone-backup-watchdog.sh: if
rclone-backup.service is active but total transferred bytes (global core/stats on the rc API)
haven't moved for 15 minutes, it restarts both rclone-rcd.service (clears any stuck/orphaned job —
this is the actual daemon holding the work, not the thin wrapper) and then rclone-backup.service
(--no-block, load-bearing — see below). Kept as a safety net even after the RAM fix, in case
memory pressure returns under a larger folder set later. State kept in
/var/lib/rclone-backup/watchdog-state.json, cleared whenever the service isn't running.
Two watchdog design bugs found and fixed while chasing this (2026-07-03):
- Wrong stats-group key. Per-job progress polling queried
core/statsunderjob/<jobid>, but rclone tracks stats under whatever_groupname the job was submitted with. Made a perfectly healthy sync look stalled at 0 bytes for 22+ hours in its own log. Fixed by using the samegroupvariable consistently. Lesson: distrust the per-run log's "progress bytes=" line during an incident; cross-check with unfilteredcore/statsfirst. - Watchdog restarted only the thin wrapper, and blocked doing it. The actual
rclone syncwork runs insiderclone-rcd.service, notrclone-backup.service— restarting the wrapper alone left any stuck job orphaned insidercdwhile a new wrapper submitted a duplicate job on top. Worse,systemctl restart rclone-backup.service(no--no-block) blocks until the new invocation's long-runningExecStartexits — which could be hours — so the watchdog's own oneshot service never logged "Finished," andOnUnitActiveSec(which schedules relative to the previous run finishing) never fired again. The watchdog silently disabled itself after exactly one use. Fixed: restartrclone-rcd.servicefirst, thenrclone-backup.servicewith--no-block.
Related
- Backups — this job supersedes the disabled restic-on-USB backup
- Hubris host — owns
/mnt/library - Media permissions — read-only consumer of
/mnt/library - Containers index
Changelog
2026-07-03 — root cause found (OOM, not Proton); RAM bumped to 2 GiB; full folder set completed
What looked like repeated silent "protondrive stalls" turned out to be rclone-rcd.service
getting OOM-killed under the original 1 GiB RAM allocation — confirmed via
journalctl -u rclone-rcd.service showing killed by the OOM killer at the exact freeze point.
Bumped the LXC to 2 GiB RAM (live, no reboot). After the bump: cloud (287G) completed cleanly
(exit 0), documents (249M) completed with 1 minor error (259.7 MB transferred), repos (83M)
completing as of this entry. Also fixed two real watchdog bugs found while chasing this (wrong
stats-group key making a healthy sync look frozen; watchdog restarting only the wrapper with a
blocking systemctl restart, causing it to silently disable itself after one use) — see "Known
issue" above for full detail. The watchdog is kept as a safety net going forward even though the RAM
bump addresses the actual root cause.
2026-07-02 — runner rewritten to submit jobs via the rc API (GUI job visibility)
The original runner (rclone sync invoked as a standalone CLI subprocess) was invisible to the Web
GUI's Jobs panel — the GUI only tracks work submitted through its own rcd process. Rewrote
/usr/local/sbin/rclone-backup.sh in Python, submitting each folder via POST /sync/sync with
_async: true against http://127.0.0.1:5572 (the running rclone-rcd.service), then polling
POST /job/status + POST /core/stats (both must be POST — GET-with-querystring 404s on this
rc API) until finished, logging periodic progress snapshots and the same runs.jsonl summary line
as before. Verified live: submitted job visible in POST /job/list's runningIds while running,
completed cleanly (success: true) once done. Deployed via atomic rename (write-then-mv) rather
than truncating in place, specifically so it wouldn't risk corrupting the still-running original
cloud+documents+repos sync mid-flight (verified after the fact: that sync's bash process was
unaffected, kept running to completion under the old in-memory script content). The already-running
scheduled sync from before this change is a standalone process and won't retroactively appear in the
GUI; every run after this point will.
2026-07-02 — Proton Drive auth fixed; real folder set enabled; first live sync
Initial rclone config failed 2FA (422 ... auth/v4/2fa) because a live 6-digit TOTP code was
entered instead of the TOTP secret — reconfigured with the secret, auth now works
(rclone lsd proton: lists the Drive). Verified end-to-end with a real sync of /mnt/library/notes
(219 objects, 5.964 MiB, exit 0) — confirmed files land as plain, browsable objects on Proton (not
an opaque archive), matching the plain-mirror + Proton-E2E design. Checked Proton quota (2 TiB
plan, 1.945 TiB free) before enabling a large folder. folders.list set to the real selection:
cloud (287G), documents (249M), repos (83M); a full sync of that set was kicked off via the
actual rclone-backup.service unit (not an ad-hoc call) to validate the real monthly path early
rather than waiting for the Aug 1 timer. Tracked-repo step (dtoro/rclone on gitea) deferred by
choice — runner/units/folders.list remain plain files on the LXC for now; the repo package stays
staged at /root/rclone-repo for later.
2026-07-01 — provisioned; enrolled
LXC 132 created (Debian 13, privileged, 192.168.8.214, /mnt/library read-only). rclone v1.74.3
installed from the official binary (protondrive backend present). Runner + monthly timer +
folders.list deployed; rclone Web GUI (rcd, LAN-only no-auth) live on :5572. Enrolled into
homelab-context (--no-mesh, LAN-only issuance): age key issued, inventory finalized, shared
secrets granted, homelab whoami + homelab secret hello verified. Gitea ALLOWED_HOST_LIST
updated to include 192.168.8.214. Hit and fixed a pct exec PATH gotcha (see below). Proton Drive
remote, dtoro/rclone tracked repo + webhook, and the secrets/protondrive.yaml escrow remain
operator-run follow-ups (credentialed steps — Proton password/2FA, repo creation). Restic-on-USB
backup deprecated in the same change.