Skip to content

Architecture

How the homelab is wired — host, topology, routing, and data flows. For domain context, conventions, and gotchas see CONTEXT.md (the two are complementary and both kept current); for the full per-stack inventory see the Stacks table in CONTEXT.md.

Single host. Everything runs on av as Docker Compose stacks (plus vLLM as user systemd units). The earlier Pi (Zigbee/Thread/voice/etc.) has been fully migrated off and retired — the Pi was rebuilt into an unrelated retro console (docs/retro-box/).

Host: av (ascertain-verse)

Framework Desktop · AMD Strix Halo "AI Max+ 395" · 128 GB unified RAM (~96 GB usable as VRAM) · CachyOS (Arch) · LUKS root. Admin user skill-lava (docker group; sudo requires a password — automation never uses it). A guest-account Sunshine/Moonlight gaming setup shares the box and is off-limits (docs/gaming/): don't touch display/session/udev config or ports 47984–48010 / 1716.

Layers

                       Internet / tailnet
        AdGuard DNS  ─────────┤  *.fmm.house → av LAN IP (local fallback)
        (LAN :53)             │
        Traefik  :80/:443  ── *.fmm.house wildcard TLS (LE DNS-01 / porkbun)
          │   owns the `proxy` bridge net (172.30.0.0/24)
   ┌──────┴───────────────── two routing paths ─────────────────┐
   │ container labels (most services)   │ file provider (a few)  │
   │ traefik.* on the proxy network     │ dynamic/*.yaml →        │
   │                                    │ 192.168.86.49:<2001-2005>│
   ▼                                    ▼
 apps: ha · homarr · forgejo · arcane · immich · matrix · ocis · peertube · frigate ·
       audiobookshelf · calibre-web-automated · beszel · uptime-kuma · postmoogle ·
       music · trmnl · …
 data: bind-mounted config (committed) · runtime in named volumes / gitignored data/
  • Edge: AdGuard (LAN DNS + ad-block) → Traefik (reverse proxy + TLS). Traefik reaches host-networked services (HA, matter-server) over the proxy bridge.
  • Routing: most services are reached only via Traefik container labels and publish no host port. A few are routed by file-provider entries (stacks/traefik/dynamic/*.yaml) pointing at a published host port in 2001–2100. Protocol ports (53/80/443/1883/…) are pinned. Full map: docs/host-port-map.md.
  • Data: one compose file per stack (stacks/<name>/compose.yml, explicit name:); bind- mounted config is committed, runtime data lives in volumes or gitignored data/.

Home Assistant + radios

HA runs host-networked on av. Zigbee via Zigbee2MQTT (ZBT-2 coordinator; migrated from the Pi's ZHA without re-pairing — docs/runbooks/zigbee-migration.md). Mosquitto is the MQTT bus (LAN only). Matter server runs on av; a Thread border router (otbr) is defined in stacks/ha but not yet set up (its SkyConnect radio isn't connected) — docs/runbooks/thread-multiprotocol.md.

Local-AI pipeline

vLLM serves the local LLM (Ornith) as user systemd units (not compose): :8001 model → :8002 think-proxy → :8010 tool-router. Two consumers: the HA voice pipeline (STT faster-whisper / TTS piper) and the pi coding agent (code.fmm.house). Full detail — model, proxies, MCP tool-shaping — in CONTEXT.md.

Observability

up.fmm.house (Uptime Kuma, availability) · bz.fmm.house (Beszel hub + local agent, metrics & btop-style container view) · Diun (image-update notifier → Matrix + MQTT→HA phone push) · Docker HEALTHCHECKs on ~every container. Backups are not set up yet (deferred to a separate off-box target). See docs/runbooks/monitoring.md.

Network model (fmm.house)

  • DNS — one public answer, reachable two ways (split-horizon retired 2026-07-16; full spec docs/runbooks/dns.md): the porkbun zone is authoritative for everyone, publishing per name A *.fmm.house/apex → 192.168.86.49 (LAN IPv4) and two AAAAs — fd00:6176::49 (av's LAN IPv6 ULA) and fd7a:…:4001:3507 (av's native Tailscale IPv6). The two LAN addresses are reached on-LAN directly or off-LAN via an approved Tailscale subnet route (192.168.86.49/32 + fd00:6176::49/128); fd7a is the fallback for peers those routes don't reach — route-less peers and node shares (subnet routes don't cross a share). Every service is thus reachable on the LAN or the tailnet and nothing is internet-routable (all private; no port-forward; the wildcard hides the service list). Traefik does all routing + the apex→www redirect. AdGuard's rewrite (A .49 + AAAA fd00:6176) is now just a local fallback for the WAN-down case, not load-bearing. A public fmm.house MX 10 mail.fmm.house routes remote-tailnet mail to Postmoogle (:25); external senders resolve to the private addresses and fail (no public inbound mail). Note: AdGuard's UI rewrites are A/AAAA/CNAME-only and shadow other record types, so internal mail uses the apex A as an implicit MX rather than an internal MX record (docs/runbooks/email-to-matrix.md, ADR-0006).
  • TLS: one *.fmm.house Let's Encrypt wildcard, DNS-01 via the porkbun API (stacks/traefik/.env), auto-renewed by Traefik.
  • Tailscale: tailnet anaconda-perch.ts.net; av = 100.98.53.7.
  • ufw: LAN-scoped for 80/443/53/8123, tailscale0 for 80/443, and a blanket allow from the proxy bridge 172.30.0.0/24 (covers new Traefik-fronted services). Added manually with sudo.

Known cruft (cleanup at leisure, needs sudo)

  • av:~/ai/ — root-owned empty mount-skeleton dirs from a deleted compose project (containers adopted into stacks/ai).
  • ~/homelab/comfy-env (~18 GB) + ~/homelab/.venv — legacy ComfyUI python envs, gitignored.
  • The pre-repo compose draft is preserved at docs/attic/ai-compose-draft.yml.