Deferred Phase-1-completion task from the de-platform-from-GitHub roadmap (vimwiki/diary/2026-05-03.md). Documents: - per-host role + current services (sunken-ship, phantom-ship, vps-relay, distant-shore, foreign-port, daniel-macbook-air, wsl); - ZT mesh topology + ASCII overview; - the auto-rebuild path (dm-pull-deploy push from sunken-ship → pull/ rebuild on roles.default hosts within ~15 m); - the manual clan-cli flow, including the env -u SSH_AUTH_SOCK and --no-check gotchas we hit in practice; - the vps-relay reverse-proxy pattern for new public apps; - SSH key quick-reference. |
||
|---|---|---|
| .. | ||
| daniel-macbook-air.nix | ||
| distant-shore-hardware.nix | ||
| distant-shore.nix | ||
| phantom-ship-hardware.nix | ||
| phantom-ship.nix | ||
| README.md | ||
| server-install.nix | ||
| sunken-ship-hardware.nix | ||
| sunken-ship.nix | ||
| vps-relay.nix | ||
| wsl.nix | ||
Hosts
Per-host NixOS configs for the homelab and admin Mac. Each <host>.nix
declares the host's role and services; the <host>-hardware.nix siblings
(where present) describe disks, kernel modules, firmware. Bootstrap +
disko configs live one level up in ../.
Topology
┌────────────────────────────────────────────┐
│ vps-relay (Hetzner, public IP, ZT peer) │
public traffic ──TLS:443──────│ Caddy + Let's Encrypt → reverse_proxy │
│ over ZeroTier to a clan backend │
└──────────────────┬─────────────────────────┘
│ (ZeroTier mesh)
│
┌──────────────────────┬───────────┴───────────┬──────────────────────┐
│ │ │ │
sunken-ship phantom-ship distant-shore foreign-port
(LAN, wifi) (LAN, wired) (LAN, wifi) (LAN, wifi)
ZT controller NAT for blank slate blank slate
media + mulbo rusty-anchor (room to grow) (room to grow)
services hub
── outside the clan ──────────────────────────────────────────────────────────────────────
Daniel-Macbook-Air rusty-anchor
(admin, runs clan-cli) (downstream of phantom-ship's NAT)
ZeroTier IPv6 addresses for the four clan machines are declared in
../../flake-modules/clan.nix (sunkenShipZTv6 / phantomShipZTv6 /
vpsRelayZTv6 / etc.). They land in every host's /etc/hosts as
<machine>.clan so data-mesher and ad-hoc SSH can resolve over the mesh.
Hosts
sunken-ship · media + ZT controller
- Hardware: see
sunken-ship-hardware.nix. WiFi-only, no LUKS (boots unattended). - Network: LAN over WiFi, on the ZT mesh as the controller (manages
ZT membership for the whole fleet). ZT IPv6 is the clan's "internet"
target for
clan machines update. - Role: media + the long-running personal services that don't fit on phantom-ship.
- Current services:
navidrome(subsonic API,/srv/music),uxplay(AirPlay receiver),mulbo-server(+-pull/-backfill/-enrichtimers),fitness-bot(+-pull/-shipyardvariants),dm-pull-deploy-push(announces origin/main rev to the mesh every 15 m).
phantom-ship · services hub + LAN NAT
- Hardware: see
phantom-ship-hardware.nix. WiFi for WAN, wired ethernet (enp0s31f6) serves the lab subnet (NAT + dnsmasq forrusty-anchor). - Network: LAN over WiFi, on the ZT mesh. Backends are exposed only on
the ZT interface (
firewall.interfaces."zt+".allowedTCPPorts = [ … ]) so vps-relay's Caddy can reach them. WAN side stays closed. - Role: where new self-hosted apps default to going. Hosts a growing list of mini-app backends + a couple of long-running daemons.
- Current services:
forgejo(git.dannydannydanny.me),claude-channels(Telegram bridge for@HarakatBot),hara-gmail-mcp+hara-heartbeat(timer), Mini-App backends (shelfish,scuttle,bananasimulator,komtolk,escape-hormuz,bon),ollama(local LLM),shipyard,dnsmasq(lab subnet DHCP/DNS).openclaw-gatewayis disabled — superseded byclaude-channelsbut kept for easy rollback.
vps-relay · public reverse proxy
- Hardware: Hetzner Cloud vServer (BIOS-boot, virtio). Disk via
../disko-cloud.nix. - Network: public IP
89.167.39.251. Inbound: SSH/22, HTTP/80, HTTPS/443 only. fail2ban guards SSH. Outbound to clan backends over ZT. - Role: terminates public TLS, reverse-proxies subdomains over ZT to
whichever clan host runs the backend. No application data ever lands
here — this box is a relay. New public app = add a
virtualHostsentry + a GoDaddy A record pointing at89.167.39.251. - Current vhosts:
navidrome.,bbbot.,shelfish.,scuttle.,bananasimulator.,komtolk.,git.,escapehormuz., etc.
distant-shore · ThinkPad X13 Gen 2, blank slate
- Hardware: see
distant-shore-hardware.nix. Intel i5-1145G7, 16 GB. WiFi-only, headless, no LUKS. Secure-Boot-chained boot (shim + MOK, see comments indistant-shore.nix). - Network: LAN over WiFi, on the ZT mesh.
- Role: to be assigned. In the clan inventory; auto-rebuilds via dm-pull-deploy. Drop a service in to give it a purpose.
foreign-port · laptop, blank slate (WIP)
- Hardware: see
foreign-port-hardware.nix. WiFi-only, headless, no LUKS. Vendor-signed-shim boot chain. - Status: still being wired up — not in the clan inventory yet.
- Role: to be assigned, same flow as
distant-shore.
daniel-macbook-air · admin
- Hardware: MacBook Air (the daily driver).
- Role: outside the clan. Runs
clan machines updateto push to the servers + holds the SSH keys that authorize root@ on each clan host. Also a ZT peer.
wsl
- Role: WSL development environment (legacy / occasional).
Deployment
Automatic (the default)
dm-pull-deploy (clan-community module wired in clan.nix):
- Push announcement: sunken-ship's
dm-pull-deploy-pushtimer runsdm-send-deployevery 15 m. It signs and broadcasts the currentorigin/mainrev over the data-mesher gossip protocol. - Pull + rebuild: each
roles.defaultmachine (currentlysunken-ship,phantom-ship) runs a.pathwatcher that fires when the gossiped rev changes; itgit fetches andnixos-rebuild switches.
So a push to origin/main rolls out within ~15 m on the two
production hosts. No SSH-from-Mac required.
vps-relay and distant-shore are not in roles.default — they
need a manual deploy (see below) until/unless their role changes.
Manual
From ~/dotfiles on the Mac:
nix run 'git+https://git.clan.lol/clan/clan-core#clan-cli' -- \
machines update <host>
Caveats encountered in practice:
- The Mac's
ssh-agentoften has the wrong key loaded for clan deploys. Prefix withenv -u SSH_AUTH_SOCKto force~/.ssh/configidentity selection. - A nixpkgs bump may register a new generation but refuse to live-switch
due to "switch inhibitors". Add
--no-checkto force. vps-relayonly accepts~/.ssh/id_ed25519_sunken_ship(the Mac's copy of sunken-ship's authorized key). The agent's other keys won't open it.
Putting both together:
env -u SSH_AUTH_SOCK nix run 'git+https://git.clan.lol/clan/clan-core#clan-cli' \
-- machines update phantom-ship --no-check
From sunken-ship
vps-relay was originally only reachable from sunken-ship's SSH key.
That still works as a fallback — SSH to sunken-ship and run the same
nix run … -- machines update vps-relay command from /etc/dotfiles
there. The dotfiles checkout at /etc/dotfiles is maintained by
dm-pull-deploy.
Public traffic pattern
user → DNS *.dannydannydanny.me → 89.167.39.251 (vps-relay)
→ Caddy (Let's Encrypt, ports 80/443)
→ reverse_proxy http://[<backend ZT IPv6>]:<port>
→ service on sunken-ship or phantom-ship
To add a new public app:
- Add a
virtualHostsentry tovps-relay.nixpointing at the backend's ZT IPv6 and port. - Add the GoDaddy A record
<sub>.dannydannydanny.me → 89.167.39.251. - Run the backend on the chosen host. Either:
- bind to
127.0.0.1:<port>(if backend + Caddy are co-resident — not the case here), or - bind to
0.0.0.0(or::) and add the port tonetworking.firewall.interfaces."zt+".allowedTCPPortson the backend host so only the ZT interface accepts inbound.
- bind to
- Push dotfiles. Production hosts auto-rebuild via dm-pull-deploy.
vps-relay needs a manual
clan machines update vps-relay.
SSH keys (quick reference)
~/.ssh/id_ed25519_phantom_ship(Mac) — authorized asdanny@androot@on phantom-ship.~/.ssh/id_ed25519_sunken_ship(Mac) — authorized asdanny@(and via root mirror) on sunken-ship; also the authorized key onvps-relay.- sunken-ship
~/.ssh/id_ed25519— sunken-ship's own key; used by cluster-internal ops (mulbo-pull, dm-send-deploy, fallback path for vps-relay deploys). ~/.ssh/id_ed25519_github(Mac) — GitHub auth, not clan.
Authorized-keys lists live in each host's users.users.{danny,root}.openssh.authorizedKeys.keys.