Warning
This documentation was generated by AI (Claude Sonnet 4.6). It reflects the state of the repo as understood at generation time. Verify against actual files before making changes.
Home lab infrastructure managed as code. Two separate systems: a NixOS server (node4) running Komodo for Docker container management, and a Talos Kubernetes cluster (homelab).
lab/
├── flake.nix # Nix flake entry point
├── nixos/
│ ├── hosts/node4/
│ │ ├── configuration.nix # node4-specific config (IP, secrets, user, auto-upgrade)
│ │ └── hardware-configuration.nix
│ └── modules/
│ ├── common.nix # Shared config (SSH, packages, firewall, GC)
│ ├── containers.nix # All Docker containers + Komodo setup
│ └── networking.nix # (currently empty placeholder)
├── komodo/
│ ├── servers.toml # Komodo server definitions (sync config)
│ ├── stacks.toml # Komodo stack definitions (sync config)
│ └── stacks/
│ └── beszel/
│ ├── agent/compose.yaml # Beszel monitoring agent
│ └── hub/compose.yaml # Beszel monitoring hub
├── talos/ # Talos Kubernetes cluster config
│ ├── talconfig.yaml # Cluster definition (talhelper)
│ └── ...
├── kubernetes/ # Flux GitOps manifests
└── .github/workflows/
└── flake-update.yaml # Auto-updates flake.lock every Sunday
Hardware: Dell OptiPlex 7050 Micro
IP: 192.168.40.10/24 (static, interface enp0s31f6)
Gateway: 192.168.40.1
DNS: 192.168.10.1
From your local machine (with Nix + deploy-rs available), or directly on node4:
# From node4 itself
sudo nixos-rebuild switch --flake github:Evan-2007/lab#node4
# From a local clone targeting node4 remotely
nixos-rebuild switch --flake .#node4 --target-host [email protected] --use-remote-sudonode4 automatically pulls and applies the flake from GitHub every day at 4:00 AM. Managed by system.autoUpgrade in configuration.nix. To disable temporarily:
sudo systemctl disable nixos-upgrade.service- Username:
evan - Passwordless sudo is enabled
- SSH password auth is currently on — add keys to
openssh.authorizedKeys.keysinconfiguration.nixand setPasswordAuthentication = falseincommon.nixwhen ready
Secrets are encrypted with sops-nix using Age.
- Age key location on node4:
/root/.config/sops/age/keys.txt - Encrypted secrets file:
nixos/secrets/node4.yaml
Secrets used on node4:
| Secret name | Used by |
|---|---|
komodo_jwt_secret |
Komodo core (JWT signing) |
komodo_db_password |
Komodo core (FerretDB auth) |
At boot, a oneshot systemd service (komodo-env-setup) reads these from /run/secrets/ and writes them to /run/komodo/core.env, which the komodo-core container loads via environmentFiles.
- Edit the encrypted file:
sops nixos/secrets/node4.yaml - Add the new key/value
- Reference it in
configuration.nixundersops.secrets - The decrypted value will appear at
/run/secrets/<name>at boot
Komodo runs on node4 and manages Docker stacks across the lab. It consists of three containers all on a dedicated Docker network (komodo).
| Container | Image | Port | Purpose |
|---|---|---|---|
komodo-ferretdb |
ghcr.io/ferretdb/ferretdb:1 |
internal only | MongoDB-compatible DB (SQLite backend) |
komodo-core |
ghcr.io/moghtech/komodo-core:2 |
9120 |
Komodo web UI + API |
komodo-periphery |
ghcr.io/moghtech/komodo-periphery:2 |
8120 |
Agent that executes actions on node4 |
All defined in nixos/modules/containers.nix.
Browser / Komodo CLI
│
▼
komodo-core :9120 ──────────────────────────────► komodo-periphery :8120
│ (inbound mode, HTTP) │
▼ ▼
komodo-ferretdb :27017 Docker socket / repos / stacks
(SQLite backend)
Important: Periphery runs in inbound mode — core connects to periphery, not the other way around. Do not set PERIPHERY_CORE_ADDRESS in the periphery environment; doing so enables outbound mode and also requires PERIPHERY_CONNECT_AS to be set, otherwise periphery exits immediately (~2 seconds) with a warning.
SSL is disabled on periphery (PERIPHERY_SSL_ENABLED = false). The connection between core and periphery is plain HTTP on the internal network. If you need SSL later, generate certs, place them in /var/lib/komodo/ssl/, and remove the SSL env var override.
systemd services start in this order:
docker.service
└── docker-network-komodo (creates the komodo Docker network)
├── docker-komodo-ferretdb
│ └── docker-komodo-core (waits for ferretdb + komodo-env-setup)
└── docker-komodo-periphery
Komodo uses Noise protocol keys for authentication. The core's public key is shared with periphery via a shared Docker named volume (komodo-core-keys):
- Core writes its keys to volume
komodo-core-keysmounted at/config/keys/ - Periphery reads core's public key from the same volume mounted at
/config/core-keys/ - Periphery env:
PERIPHERY_CORE_PUBLIC_KEYS = "file:/config/core-keys/core.pub"
If you wipe the komodo-core-keys volume, restart both containers so periphery picks up the new key.
komodo/servers.toml and komodo/stacks.toml are sync config files — Komodo reads these from the repo to keep its state in sync.
komodo/servers.toml — registers node4 as a managed server:
[[server]]
name = "node4"
[server.config]
address = "http://192.168.40.10:8120"komodo/stacks.toml — defines which Docker Compose stacks to deploy:
[[stack]]
name = "bezel-agent"
[stack.config]
server = "node4"
repo = "Evan-2007/lab"
branch = "main"
run_directory = "./komodo/stacks/beszel/agent"The run_directory is relative to the repo root. If you add new stacks, the compose files must live under komodo/stacks/<name>/ in this repo and the path in stacks.toml must match exactly (case-sensitive).
Periphery exits immediately (Duration ~2s):
journalctl -u docker-komodo-peripheryIf you see:
WARN: 'core_addresses' are defined for outbound connection, but missing 'connect_as'
→ PERIPHERY_CORE_ADDRESS is set in the environment in containers.nix. Remove it — periphery should run in inbound mode only.
Core can't connect to node4 / "Connection refused" on port 8120:
- Verify
PERIPHERY_SSL_ENABLED = "false"is set (SSL is on by default; using HTTP against an SSL server causes connection failures) - Check periphery is running:
systemctl status docker-komodo-periphery - Verify the port is bound:
ss -tlnp | grep 8120
"IO error: Connection refused" on FerretDB:
Usually a startup race — core started before FerretDB was ready. Force restart in order:
sudo systemctl restart docker-komodo-ferretdb
sleep 5
sudo systemctl restart docker-komodo-coreStack deploy fails: "Failed to read file contents from /repo-cache/...":
The run_directory in stacks.toml doesn't match the actual file path in the repo. Check:
- The path is relative to the repo root (e.g.
./komodo/stacks/beszel/agent, not./stacks/beszel/agent) - Spelling matches exactly — the monitoring tool is
beszel, notbezel
Restarting all Komodo containers:
sudo systemctl restart docker-komodo-ferretdb docker-komodo-core docker-komodo-peripheryA separate 3-node Kubernetes cluster managed by Talos and configured with talhelper.
| Node | IP | Role |
|---|---|---|
talos-cp |
192.168.20.10 |
Control plane |
talos-worker1 |
192.168.20.11 |
Control plane (scheduling enabled) |
talos-worker2 |
192.168.20.12 |
Control plane (scheduling enabled) |
- Kubernetes endpoint:
https://192.168.20.10:6443 - CNI: Cilium (kube-proxy disabled, configured via encrypted patch
talos/patches/cilium.sops.yaml) - Pod CIDR:
10.244.0.0/16 - Service CIDR:
10.96.0.0/12 - GitOps: Flux, manifests in
kubernetes/
# Regenerate clusterconfig/ from talconfig.yaml
talhelper genconfig
# Apply to a node
talosctl apply-config --nodes 192.168.20.10 --file talos/clusterconfig/homelab-talos-cp.yamlflake-update.yaml — runs every Sunday at midnight and on every push to main. Updates flake.lock and auto-commits it. node4's daily 4 AM auto-upgrade then picks up the new lockfile, keeping packages current.
| Subnet | Purpose |
|---|---|
192.168.40.0/24 |
node4 / NixOS segment |
192.168.20.0/24 |
Talos Kubernetes cluster |
192.168.10.0/24 |
DNS / other infrastructure |