Architecture
This page is the end-to-end mental model of avocado. Everything else is detail on one of these layers.
The layers
avocado is a single physical machine wearing several hats at once:
flowchart TB
subgraph HW[Physical box: avocado - Intel i7-8550U, UEFI]
subgraph OS[NixOS - built from this flake]
base[Base system: nix, gc, firewall]
desk[Stats kiosk: cage + btop, plus Home Manager]
net[Tailscale + Cloudflare Tunnel]
k3s[k3s server - single node]
mon[Host metrics timers: ZFS + SMART]
end
subgraph ZFS[ZFS pool rpool - striped, no redundancy]
root[root fs at slash]
nixds[nix store]
vards[var - logs, k3s state]
homeds[home - user data]
end
end
OS --- ZFS
k3s --> workloads[Workloads: Immich, monitoring stack]
Each layer is defined by a NixOS module (see NixOS & modules) and imported by hosts/avocado/default.nix.
How the flake wires together
flowchart LR
flake[flake.nix]
flake --> nixos[nixosConfigurations.avocado]
flake --> home[homeConfigurations.rithviknishad@avocado]
flake --> shell[devShells.default]
nixos --> hw[hosts/avocado/hardware.nix]
nixos --> disko[hosts/avocado/disko.nix]
nixos --> mods[modules/*.nix]
nixos --> user[users/rithviknishad.nix]
mods --> hm[modules/home-manager.nix]
hm --> homeprofile[home/rithviknishad]
home --> homeprofile
shell --> tools[just, sops, age, kubectl, helm, helmfile, nixos-anywhere]
Two ways the same Home Manager profile is used:
- System-wide via
modules/home-manager.nixduring a fullnixos-rebuild(nh os switch). - Standalone via
homeConfigurations."rithviknishad@avocado"so you can iterate on just your dotfiles withnh home switch— no full rebuild.
How a public request reaches a service
This is the single most important flow to understand. There are no inbound ports open to the internet — cloudflared dials out to Cloudflare and the tunnel carries traffic back in.
sequenceDiagram
participant U as User (browser)
participant CF as Cloudflare edge (TLS)
participant CD as cloudflared (on avocado)
participant T as Traefik (k3s ingress :80)
participant S as Service pod (e.g. Immich)
U->>CF: https://photos.rithviknishad.dev
Note over CF: TLS terminates here
CF->>CD: tunnel (outbound-established)
CD->>T: http://localhost:80 (Host: photos.rithviknishad.dev)
T->>S: route by Host header
S-->>U: response back through the tunnel
Key points:
- TLS terminates at Cloudflare’s edge — no cert-manager needed on the box.
cloudflaredforwards every mapped hostname tolocalhost:80, where Traefik routes byHostheader to the right k8s Ingress.- The same services are reachable privately over Tailscale by sending the
Hostheader tohttp://avocadodirectly (bypassing Cloudflare).
See Networking for the full routing table.
Where data lives
flowchart TB
pool[ZFS pool: rpool]
pool --> root[slash - OS root]
pool --> nixds[nix - store]
pool --> vards[var - logs, k3s state]
pool --> homeds[home - user data]
vards --> lp[k3s local-path provisioner]
lp --> pvcs[PVCs: Immich library/db, VictoriaMetrics, VictoriaLogs]
k3s’s built-in local-path provisioner carves PersistentVolumes out of the host filesystem (under /var), which sits on the ZFS rpool. That means every PVC ultimately lives on the no-redundancy stripe — losing either disk loses it all. This is exactly why the monitoring stack puts so much weight on ZFS pool health and SMART alerts.
Secrets flow (build + boot time)
flowchart LR
admin[Admin Mac - age key] -->|edit + encrypt| repo[(secrets/*.yaml in repo)]
repo -->|sops-nix at activation| hostkey[avocado host age key]
hostkey --> runsecrets[run-secrets mounts]
runsecrets --> svc[Services: user password, tailscale, k3s, cloudflared]
Everything sensitive is committed encrypted. The box decrypts at activation using its own age key at /var/lib/sops-nix/key.txt (never in the repo). Full details on the Secrets page.
Design decisions worth knowing
- One striped ZFS pool, no redundancy. Chosen for maximum capacity (~342 GB) from two mismatched disks. The tradeoff: any single disk failure destroys the whole pool including the OS. Off-box
zfs sendbackups are the safety net. - Kiosk + server on one box. The machine never sleeps (sleep targets are masked) and the screen never locks or blanks — it permanently shows btop — so services stay reachable and the display doubles as a status panel.
- k3s with bundled add-ons on. Traefik, ServiceLB, and local-path are left enabled for an easy first workload rather than swapping in heavier alternatives.
- Push-based public access. Cloudflare Tunnel avoids the need for a static IP, port forwarding, or a public firewall hole.
- Secrets in-repo, encrypted. sops-nix keeps the config fully declarative without leaking plaintext — the box decrypts at activation with a host key that never lives in the repo.