Pros / Cons per substrate — for this homelab¶
Honest tradeoffs for each option, judged against the real constraints (Windows + Docker Desktop, no Linux servers, churning DHCP, isolated subnet, local disks only). A technology scoring badly here may be excellent elsewhere — this is environment-specific, not a general ranking.
Docker Compose (in Git) — recommended baseline¶
Pros
- First-class on Docker Desktop / Windows; zero extra control plane.
- Declarative and diffable: the desired state is the committed YAML.
- restart: unless-stopped + healthcheck gives per-node self-healing.
- Trivial mental model; day-2 ops is "edit YAML, re-run one script".
- Completely indifferent to DHCP churn and subnet islands (single-host scope).
- Recovers to a known-good state from a clean git clone.
Cons - No cross-node scheduling — if a host dies, you manually bring the stack up elsewhere (the DR scripts automate the stateful part of that). - No built-in service discovery across machines (foundation/ + stable names fill this gap). - Scaling identical replicas across hosts is manual.
Verdict: best fit. This is the substrate to run.
Docker Swarm — only cluster worth a later look, stateless only¶
Pros
- Ships inside the Docker engine — no separate install, low ceremony.
- docker stack deploy reuses Compose file syntax you already know.
- Real cross-node rescheduling for stateless services.
- Much lighter than k8s to stand up and operate.
Cons - Manager quorum uses raft — same sensitivity to DHCP churn / partitions as etcd. On this network it will flap until addressing is rock-solid. - No answer for stateful Postgres (local-disk-only ⇒ data can't follow a task). - Smaller ecosystem / slower feature pace than k8s; less job-market value. - Adds a distributed control plane you must babysit for modest gains at 4 nodes.
Verdict: optional Stage 3, stateless workloads only, after weeks of stable addressing. Never Postgres.
k3s — the learning path¶
Pros
- Lightweight, single-binary k8s; the sane way to run k8s in a homelab.
- Real Kubernetes API — genuine, transferable practice for a k8s job
(Deployments, Services, Ingress, StatefulSets, operators, kubectl).
- Bundles a lot (containerd, flannel, traefik, local-path storage) so a lab
comes up fast.
Cons (as production here) - Control plane is Linux ⇒ runs in WSL2/VMs on these Windows hosts; extra layer that suspends/loses clock on laptop sleep. - etcd/quorum vs churning DHCP + isolated subnet = fragile; needs static IPs / stable names as a hard prerequisite. - Does not replicate Postgres data. A StatefulSet + local-path PV pins data to one node; with local disks only, a reschedule can't carry the data. - No always-on Linux node here ⇒ nothing is a dependable server.
Verdict: excellent lab for learning; not the production substrate here.
Lives in kubernetes/ with not-prod-here banners.
Full Kubernetes (kubeadm) — not here¶
Pros - The industry-standard, maximal-control, maximal-ecosystem platform. - Highest job-market signal.
Cons (here) - All of k3s's cons, amplified: you now also own etcd, certs, CNI, upgrades, and the full day-2 surface by hand. - Enormous ops burden for four home PCs with no always-on Linux nodes. - Same fundamental non-answer for stateful Postgres on local-disk storage.
Verdict: overkill and fragile. If you want the full-k8s learning experience, do it in a dedicated VM lab, not as home ops.
Bespoke Postgres DR scripts — how statefulness is actually solved¶
Pros
- Directly solves the real hard problem (protecting/recovering PG data).
- Works regardless of orchestrator; no cluster required.
- pg_dump warm standby is simple, inspectable, and cheap.
- Restore-verification turns "we have backups" into "we have backups that
provably restore".
- Primary-marker prevents split-brain (two nodes both acting primary).
Cons - RPO is bounded by dump interval (hourly ⇒ up to ~1h data loss). - Promote/failover is scripted, not instantaneous; RTO is minutes, not seconds. - You own the scripts — they must be tested, not just written.
Verdict: the DR core of this blueprint. Pairs with Compose.
Stateful vs. stateless treatment¶
The single most important framing in this whole blueprint:
-
Stateless (future daemons, the sample daemon, ingress-y things): these are the only workloads that benefit from cross-node scheduling. If one dies, another identical copy elsewhere is just as good. Compose restart policies cover single-node; Swarm (later) covers cross-node. k8s would too. Cheap to recover — no data to protect.
-
Stateful (claude-mem Postgres; Valkey to a lesser degree): recovery means protecting and restoring data, which no orchestrator does for you on local-disk-only storage. This is solved at the database layer with the bespoke DR scripts. Valkey here is treated as a cache/derived state — acceptable to lose and rebuild — so it is not given the same dump/restore rigor as Postgres (documented in the compose stack).
-
The runner is stateless and already orchestrated by GitHub — it needs neither our scheduler nor our DR. It stays out of everything.
RPO / RTO discussion¶
Definitions: - RPO (Recovery Point Objective) — how much data you can afford to lose (measured back in time from the failure). - RTO (Recovery Time Objective) — how long you can afford to be down.
For claude-mem Postgres with the hourly pg_dump warm-standby model in this repo:
| Metric | This blueprint (hourly dump + scripted promote) | If you later add streaming replication |
|---|---|---|
| RPO | Up to ~1 hour (last successful dump). Tighten by dumping more often (e.g. every 15 min) at the cost of load. | Seconds (WAL streaming), if you graduate to a replica. |
| RTO | Minutes — time to run Promote-PostgresStandby.ps1, flip the primary-marker, and restart the worker against the new primary. |
Seconds-to-minutes with an automated failover controller. |
Practical targets to adopt and test against: - claude-mem Postgres: RPO ≤ 1h, RTO ≤ 15 min. Verified by a rehearsed promote + a passing scheduled restore-verification. - Valkey (cache): RPO = "since last warm state is rebuildable"; RTO = the few seconds to start a fresh container. No dump SLA. - Stateless daemons: RPO n/a (no state); RTO = container/host restart time. - Runner: RPO/RTO owned by GitHub; out of scope.
How to keep these honest: an untested backup is not a backup. The scheduled
Test-PostgresRestore.ps1 run is what converts these numbers from aspiration
into fact. If restore-verification hasn't passed recently, assume your real RPO
is "unknown" and your RTO is "however long debugging takes at 2am".