docs: Add infrastructure overview

Document the module architecture, deployment workflows, installer, and
high-level box map.
This commit is contained in:
2026-08-01 23:51:26 +01:00
parent 390818ffdb
commit 43cf35d54e
5 changed files with 662 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
# Deployment documentation
> **Note:** these pages are a work in progress and were **agent-generated** from the repository.
> They may be incomplete or out of date — treat the Nix configuration as the source of truth.
This directory documents the boxes managed by this flake: their roles, network assignments,
hierarchy, and the services they run. For the mechanics of the repo itself (conventions, module
system internals for contributors, agent guidance), see [`AGENTS.md`](../AGENTS.md).
The two big sites follow the pattern:
```
physical host (VM host)
└── VM (for things impractical to containerise)
└── container host VM
└── NixOS containers (one per application group)
```
Not every box fits this pattern, but **colony** and **home** are organised this way.
## General
- [`architecture.md`](architecture.md) — the custom module system, `my.*` namespace, multiple
nixpkgs channels, shared module inventory.
- [`networking.md`](networking.md) — network assignments, domains, site topologies, router HA,
the AS211024 L2 mesh, BGP, WireGuard, Tailscale.
- [`deployment.md`](deployment.md) — deploy-rs, devshell commands, secrets workflow, CI.
- [`reference/nixos-options.md`](reference/nixos-options.md) — generated per-option reference for
the custom `my.*` NixOS modules.
## Site: colony (Amsterdam)
Physical host and public-infrastructure hub — see [`sites/colony/README.md`](sites/colony/README.md).
```
colony (physical VM host, ams1)
├── estuary ── edge router: WAN, firewall/NAT, DNS, BGP (AS211024), WireGuard
├── shill ──── NixOS container host ──┬── middleman (reverse proxy, ACME, nginx-sso, librespeed)
│ ├── vaultwarden (password manager)
│ ├── colony-psql (shared PostgreSQL)
│ ├── chatterbox (Matrix Synapse + bridges)
│ ├── jackflix (media stack)
│ ├── object (MinIO, Harmonia Nix cache, Sharry, HedgeDoc, wastebin)
│ ├── toot (Bluesky PDS; Mastodon disabled)
│ ├── waffletail (Tailscale subnet router / exit node)
│ ├── qclk (WireGuard management appliance)
│ ├── gam (Terraria server)
│ └── jam (raw nspawn customer container)
├── whale2 ─── podman/OCI host for game servers
├── git ────── Gitea + Gitea Actions runner
├── mail ───── Debian VM running mailcow (not NixOS)
└── darts ──── third-party/customer VM (opaque, not NixOS)
```
## Site: home
Redundant routers, VM host, storage, IoT containers and the workstation — see
[`sites/home/README.md`](sites/home/README.md). The hand-configured switch fabric (jim/dave/brian)
and the Digiweb WAN path are documented in [`sites/home/switches.md`](sites/home/switches.md).
```
h.nul.ie
├── palace (physical VM host — AMD, 100G, SR-IOV)
│ ├── river ── primary router VM (PPPoE / Digiweb WAN)
│ ├── cellar ─ NVMe-oF / SPDK storage target VM
│ └── sfh ──── container host VM ("services for home")
│ ├── hass ── Home Assistant + Frigate + MQTT (container)
│ └── unifi ─ UniFi controller (container)
├── stream (physical secondary router — Virgin Media WAN)
└── castle (workstation / gaming desktop — netboot, NVMe-oF root)
```
## Remote boxes
The edge VPSes and remote `kelder` site are indexed in [`remote/README.md`](remote/README.md).
## Mobile boxes
The laptop is indexed in [`mobile/README.md`](mobile/README.md).
## Misc
- [`misc/installer.md`](misc/installer.md) — the custom NixOS installer image.
## A note on the assignment tables
The consolidated [`Box assignments`](networking.md#box-assignments) tables in
[`networking.md`](networking.md) (one per site, between `<!-- assignments: <site> -->` markers)
are **generated from the flake** (`nixos.allAssignments`) by `nix run .#update-docs-assignments`
CI refreshes them on push. Individual box pages link to that section rather than carrying their
own table. Only the Notes column is hand-written; don't hand-edit the other cells.
+266
View File
@@ -0,0 +1,266 @@
# Architecture
This flake does **not** use the stock pattern of calling `nixosSystem` once per host in
[`flake.nix`](../flake.nix). Instead it runs a single `lib.evalModules` evaluation over its own
module tree, producing one big top-level config (`self.nixfiles`) from which the real flake
outputs (`nixosConfigurations`, `homeConfigurations`, `nixosModules`, `deploy`, …) are derived.
Per-host files declare *options* (`nixos.systems.<name>`); the machinery in
[`nixos/default.nix`](../nixos/default.nix) and
[`home-manager/default.nix`](../home-manager/default.nix) turns those into evaluated NixOS /
home-manager configurations.
## The top-level evaluation
`flake.nix`'s `outputs` builds a `nixfiles` attrset via `evalModules` over:
- An inline module that seeds `_module.args` (`lib`, `pkgsFlakes`, `hmFlakes`, `self`, `inputs`,
`pkgs'`), sets `nixos.secretsPath = ./secrets`, and sets the global deploy-rs SSH option
`deploy-rs.deploy.sshOpts = [ "-i" ".keys/deploy.key" ]`.
- `nixos/modules/misc/assertions.nix` from the unstable nixpkgs (so the top-level evaluation has
the standard `assertions` / `warnings` options).
- [`nixos/`](../nixos/default.nix) — defines `nixos.*` options (`systems`, `modules`,
`allAssignments`, `vpns`, `secretsPath`) and `mkSystem`.
- [`home-manager/`](../home-manager/default.nix) — defines `home-manager.*` options (`homes`,
`modules`) and `mkHome`.
- [`deploy-rs.nix`](../deploy-rs.nix) — defines `deploy-rs.*` and renders the deploy config.
- Every file in the `configs` list — the boxes themselves
([`nixos/boxes/`](../nixos/boxes) plus [`nixos/installer.nix`](../nixos/installer.nix); the
home-manager entries are currently commented out, see [Home-manager](#home-manager)).
The resulting `nixfiles.config` is mapped onto flake outputs:
| Top-level option (`nixfiles.config`) | Flake output |
|---|---|
| `nixos.systems.<name>.rendered` | `nixosConfigurations.<name>` |
| `home-manager.homes.<name>.configuration` | `homeConfigurations.<name>` |
| `nixos.modules` | `nixosModules` |
| `home-manager.modules` | `homeModules` |
| `deploy-rs.rendered` | `deploy` |
`nixfiles` itself is also a flake output, so anything in the top-level config can be addressed
directly — e.g. `build-iso` builds
`.#nixfiles.config.nixos.systems."<host>".configuration.config.my.buildAs.iso`. The flake also
exposes `lib` (the extended unstable nixpkgs lib, including `lib.my`), `inputs`, `nixpkgs`
(the `pkgs'` channel sets), and `overlays.default` (the custom packages from
[`pkgs/`](../pkgs/default.nix)).
Per platform (`eachDefaultSystem`), the flake produces:
- `packages` — everything from `pkgs/`, flattened.
- `checks` — every `homeConfigurations.*.activationPackage` plus deploy-rs's own `deployChecks`.
- `devShells.default` — the `numtide/devshell` from [`devshell/`](../devshell) (see
[deployment.md](deployment.md)).
- `ci.<system>` — one attr per buildable thing (`system-<name>`, `home-<name>` with `@` mangled
to `-at-`, `package-<name>`, plus `shell`), consumed by CI; `ciDrv` is a `linkFarm` of all of
them.
## Systems: `systemOpts` and `mkSystem`
Each entry of `nixos.systems` is a submodule defined by `systemOpts` in
[`nixos/default.nix`](../nixos/default.nix):
| Option | Type / default | Meaning |
|---|---|---|
| `system` | enum of `defaultSystems` | Nix platform string, e.g. `"x86_64-linux"`. |
| `nixpkgs` | one of `unstable`/`stable`/`mine`/`mine-stable`, default `"unstable"` | nixpkgs channel for the system. |
| `home-manager` | same enum, defaults to `nixpkgs` | home-manager channel. |
| `hmNixpkgs` | same enum, defaults to `nixpkgs` | nixpkgs channel used for home-manager's own pkgs when it doesn't share the system's. |
| `docCustom` | bool, default `false` | Include nixfiles' custom modules in the generated NixOS manual (slow). |
| `assignments` | attrsOf `assignmentOpts` | The box's network assignments (see [networking.md](networking.md)). |
| `extraAssignments` | attrsOf attrsOf `assignmentOpts` | Extra assignments for things not on the box itself (e.g. the routers' floating VIP entries). |
| `configuration` | custom merged type | The actual NixOS configuration module(s); merging runs `mkSystem`. |
| `rendered` | unspecified, default `configuration` | What ends up in `nixosConfigurations.<name>` — overridden for boxes built as something other than a plain system (e.g. `installer` renders `config.my.asISO`, containers render `config.my.asContainer`). |
`mkSystem` does the real work:
1. It selects the channel's nixpkgs flake (`pkgsFlakes.${config'.nixpkgs}`).
- It imports `nixos/lib/eval-config.nix` **by hand** because the flake force-sets `lib`;
otherwise `eval-config.nix` would import its own unextended copy.
- The supplied library is the channel's `pkgs.lib` extended with `lib.my` and a
`versionOverlay` that stamps `system.nixos` with the flake revision and channel.
2. `specialArgs` gives every module access to `self`, `inputs`, `pkgsFlakes`, the box's own
`pkgsFlake`, `allAssignments` (every box's assignments), and `systems` (all of
`nixos.systems`). Passing these via `specialArgs` (rather than module `imports`) avoids
infinite recursion.
3. The module list is: the home-manager channel's `nixosModules.default`, every module in
`nixos.modules` (the shared modules, see below), an inline module, and the box's own
`configuration` definitions (wrapped with `inlineModule'` so error messages keep file
provenance).
4. The inline module wires the plumbing:
- `_module.args`: `secretsPath`, `vpns` (the flake-level `nixos.vpns`), the box's own
`assignments`, and `pkgs'` — an attrset of **all four nixpkgs channels** for this box's
platform, so a module can grab a package from another channel (e.g. `pkgs'.stable.foo`).
- `system.name` is the attribute name of the box.
- `networking.hostName` / `networking.domain` default from the `internal` assignment (see
[networking.md](networking.md)).
- `nixpkgs.system` is set and the overlays computed at flake level (lib overlay + custom
packages) are passed through, so `pkgs` is imported modularly with config and overlays
applied.
- `home-manager.useGlobalPkgs` defaults to true when the system and home-manager channels
match; a warning is emitted when they deliberately differ.
- `home-manager.sharedModules` includes every module in `home-manager.modules` plus an inline
module. The inline module passes `pkgsPath` / `pkgs'`, disables the release check, and pins
`home.stateVersion` through `homeStateVersion` (`22.11` for stable-flavoured channels,
`23.05` otherwise).
5. Finally `applyAssertions` throws with all failed assertion messages (and shows warnings)
when the merged configuration is forced.
## The four nixpkgs channels
`flake.nix` builds `pkgsFlakes` (nixpkgs) and `hmFlakes` (home-manager):
| Channel | nixpkgs input | home-manager input |
|---|---|---|
| `unstable` | `nixpkgs/nixos-unstable` | `home-manager` (master) |
| `stable` | `nixpkgs/nixos-26.05` | `home-manager/release-26.05` |
| `mine` | `github:devplayer0/nixpkgs/devplayer0` (personal fork) | alias of `unstable` (no fork exists) |
| `mine-stable` | `github:devplayer0/nixpkgs/devplayer0-stable` | alias of `stable` |
Each channel's `lib` is extended with the `libOverlay` (`lib.my` + flake-utils). Two package
sets are built per channel:
- `pkgs'` — with the devshell/ragenix/deploy-rs/home-manager overlays; used for the dev shell
and `packages`, and exposed as the flake's `nixpkgs` output.
- `configPkgs'` — with just the lib + custom-packages overlays and an `allowUnfreePredicate`
(Widevine / Chromium); this is the `pkgs'` threaded into the top-level evaluation, from
which each box's `pkgs` and per-channel `pkgs'` module args (and home-manager's base
`pkgs`) are taken.
A box picks its channel with `nixpkgs = "mine"` (most boxes), `"mine-stable"` (e.g. `colony`),
etc. Inside a module, `pkgs` is the selected channel and `pkgs'` is the attrset of all four
(`pkgs'.<channel>.<attr>`).
## `lib.my` and the `my.*` namespace
[`lib/default.nix`](../lib/default.nix) extends nixpkgs `lib` with a `my` attrset. The
flake-level `lib` (`pkgsFlakes.unstable.lib` so extended) is for platform-independent flake
use only; each box gets its own channel's lib extended the same way. Contents:
- **Option helpers** — `mkOpt'`, `mkBoolOpt'`, `nullOrOpt'`, `mkDefault'` (slightly stronger than
`mkDefault`), `mkVMOverride'`, `inlineModule'` / `inlineModule`
(attach `_file` provenance), `commonOpts` (the shared `system`/`nixpkgs`/`home-manager`
options and the `moduleType` used for exported modules), `applyAssertions`, `duplicates`.
- **`lib.my.net`** — CIDR/IP math (`net.cidr.host`, `net.cidr.subnet`, `net.types.ipv4`, …)
from the `libnetRepo` input (`oddlama/nixos-extra-modules`' `netu.nix`). Used for nearly all
address arithmetic; addresses are almost never written literally.
- **`lib.my.c`** — shared constants from [`lib/constants.nix`](../lib/constants.nix): static
UID/GID assignments, kernel package selection (`kernel.lts`/`kernel.latest`), nginx config
snippets, networkd snippets (`networkd.noL3`), the binary-cache settings (`nix.cache`),
and the per-site domains/prefixes/VIPs described in [networking.md](networking.md).
- **`lib.my.dns`** — zone-generation helpers from [`lib/dns.nix`](../lib/dns.nix): `fwdRecords`
/ `ptrRecords` / `ptr6Records` rendered from `allAssignments`, plus the LUA-record helpers
(`ifaceA`, `lookupIP`) used by the home routers.
- **networkd helpers** — `networkdAssignment` (assignment → `systemd.network` network, see
[networking.md](networking.md)), `mkVLAN`, `dockerNetAssignment`.
### Misc
The remaining helpers are `mkDefaultSystemsPkgs`, `flakePackageOverlay` (wrap another flake's
package as an overlay), `isIPv6` / `parseIPPort` / `netBroadcast`, `nft` chain-name helpers,
`systemdAwaitPostgres`, `vm.*` LVM disk descriptors for the `vms` module, the typed `deploy-rs`
option definitions (`lib.my.deploy-rs`), `netbootKeaClientClasses` (iPXE/EFI DHCP client classes),
and `homeStateVersion`.
Custom NixOS / home-manager modules declare their options under the `my.*` namespace (the root
`options.my` is declared in `nixos/modules/common.nix` / `home-manager/modules/common.nix`) —
e.g. `my.secrets`, `my.build`, `my.tmproot`, `my.firewall`, `my.server`, `my.deploy`,
`my.vms`, `my.containers`.
## Shared modules
Registered in [`nixos/modules/_list.nix`](../nixos/modules/_list.nix) and applied to **every**
system (box files opt in per-feature via `my.*` options). The table below is the overview — what
each module is *for*; for the exhaustive, CI-generated per-option reference (types, defaults,
descriptions) see [`reference/nixos-options.md`](reference/nixos-options.md).
| Module | Provides |
|---|---|
| `common` | Baseline for all boxes: imports the impermanence, ragenix (age), sharry, copyparty and harmonia NixOS modules; pins `system.stateVersion`; `doas` instead of `sudo`; immutable users; nix settings (flakes, `ca-derivations`, the `nix-cache.nul.ie` substituter); declares the `my` option root. |
| `user` | `my.user` — the primary user: `users.users` + matching `home-manager.users` entry, wheel/doas, SSH authorized key from `.keys/me.pub`, shell taken from the home config, home persistence under tmproot. |
| `build` | `my.build` — alternate build targets via `extendModules`: `my.buildAs.devVM` (QEMU dev VM), `iso`, `container`, `kexecTree`, `netbootTree`/`netbootArchive`; `my.build.isDevVM` marker; `allHardware` profile toggle. |
| `dynamic-motd` | `my.dynamic-motd` — runs a script via `pam_exec` to generate the MOTD on login/ssh. |
| `tmproot` | `my.tmproot` — tmpfs `/` plus impermanence persistence (`persistence.dir`), with a `tmproot-unsaved` helper that walks the root tmpfs and lists files not covered by persistence. |
| `firewall` | `my.firewall` — nftables firewall: `tcp.allowed`/`udp.allowed` port lists, `trustedInterfaces`, NAT with `forwardPorts`, `extraRules` escape hatch; sane ICMP/ICMPv6/PMTUD rules baked in. |
| `server` | `my.server.enable` — server commonalities: getty autologin, LLMNR off, tightened fstrim timers, GUI and NixOS documentation off. |
| `deploy-rs` | `my.deploy` — per-box deploy-rs node/profile generation and the `deploy` user; see [deployment.md](deployment.md). |
| `secrets` | `my.secrets` — ragenix/agenix wiring: `files` to decrypt from `secrets/`, host `key` to encrypt for, identity paths derived from the OpenSSH host keys (persistence-aware), dev-key identity inside dev VMs. |
| `containers` | `my.containers.instances``systemd-nspawn` NixOS containers whose systems come from `nixos.systems` rendered `asContainer`, with bridge/macvlan networking, bind mounts, `hotReload` (reload instead of reboot) and a placeholder "dummy" init before first deploy. |
| `vms` | `my.vms.instances` — QEMU/KVM VMs as systemd units: TAP/bridge networking, LVM-backed disks, VFIO host-device passthrough (with udev tagging), UEFI, SPICE/TTY/QMP unix sockets under `/run/vms/`, clean shutdown via QMP `system_powerdown`. |
| `network` | Baseline networking: networkd only (`useDHCP = false`), IPv6 on, resolved domain/negative-cache settings; dev VMs get DHCP on `eth0` and an SSH port forward. |
| `pdns` | `my.pdns` — PowerDNS: authoritative server driven by BIND-style zone files (with templating and serial management) and recursor extra-settings wiring. |
| `nginx-sso` | `my.nginx-sso` — runs `nginx-sso` (instead of the stock NixOS module) and generates per-instance nginx `auth_request` include files. |
| `gui` | `my.gui.enable` (default on) — desktop baseline: graphics, polkit, swaylock PAM entry, Android udev rules, screenshot tmpdir. |
| `l2mesh` | Consumes `nixos.vpns.l2` — builds the VXLAN + IPsec layer-2 meshes between edge routers; see [networking.md](networking.md). |
| `borgthin` | `my.borgthin.jobs` — borg backups of thin-LVM snapshots to local or SSH repos on a systemd timer, with pruning. |
| `nvme` | `my.nvme` — sets the NVMe host NQN/hostid and, optionally, NVMe-oF-over-RDMA boot in the initrd. |
| `spdk` | `my.spdk` — SPDK target configuration (JSON RPC-driven) plus `spdk-rpc`/`spdk-setup`/`spdk-debug` helper tools. |
| `librespeed` | `my.librespeed` — a LibreSpeed speedtest: generated frontend (from the `librespeed-go` package in `pkgs/`) plus backend settings. |
| `netboot` | `my.netboot` — iPXE netboot server (TFTP/HTTP, menu) and the client-side loader that installs boot entries. |
Home-manager modules, registered in
[`home-manager/modules/_list.nix`](../home-manager/modules/_list.nix) and applied to every
home (including the per-user homes attached to systems via `my.user.homeConfig`):
| Module | Provides |
|---|---|
| `common` | Home baseline: `my.shell`, `my.ssh.authKeys`, `my.isStandalone` (home-manager running standalone vs inside NixOS), fish setup and completions generation, common programs. |
| `gui` | `my.gui` — the sway-based desktop: waybar, notifications, lock/saver plumbing (including the "doomsaver"), terminal and fonts. |
| `deploy-rs` | `my.deploy` for standalone homes — generates a `home` profile (home-manager activation) and deploy node; auto-disabled for NixOS-attached homes. |
| `swaync` | `my.swaync` — typed configuration module for Sway Notification Center. |
## Adding a box
1. Create a file (or directory with a `default.nix`) under [`nixos/boxes/`](../nixos/boxes)
that sets `nixos.systems.<name> = { system = "x86_64-linux"; nixpkgs = "mine"; assignments =
{ … }; configuration = { … }: { … }; };`. Nested boxes (VMs, containers) live under their
host's directory (e.g. `nixos/boxes/colony/vms/estuary/`).
2. Add the path to the `configs` list in [`flake.nix`](../flake.nix).
3. Give the box an `internal` assignment (name/domain) if it gets one — `hostName`/`domain`
default from it — and declare `my.secrets.key` if it has secrets.
4. Evaluate it with `check-system <name>` (cheap) before `build-system <name>`.
## Adding a shared module
1. Drop the file in [`nixos/modules/`](../nixos/modules) (or
[`home-manager/modules/`](../home-manager/modules)) with options under `my.*`, declared via
the `lib.my` helpers (`mkOpt'`, `mkBoolOpt'`).
2. Register it in the corresponding `_list.nix` (name → path). It is then applied to every
box and exported as `nixosModules.<name>` / `homeModules.<name>`.
## Home-manager
Home-manager follows the same option-definition and evaluation split as NixOS.
### `homeOpts`
[`home-manager/default.nix`](../home-manager/default.nix) defines each entry with these options:
| Option | Meaning |
|---|---|
| `system` | Target platform. |
| `nixpkgs` | nixpkgs channel used for the home packages. |
| `home-manager` | home-manager channel. |
| `homeDirectory` / `username` | User identity and home path. |
| `configuration` | The home-manager module definitions evaluated by `mkHome`. |
### `mkHome`
1. Calls the selected channel's `lib.homeManagerConfiguration`.
2. Supplies that channel's `pkgs'` with `config` emptied, allowing home-manager to apply package
configuration and overlays through its module system.
3. Passes `inputs`, `pkgsFlakes` and the selected `pkgsFlake` through `extraSpecialArgs`.
4. Loads every module in `home-manager.modules` plus an inline module that sets
`home.homeDirectory` / `home.username` and exposes every channel through the `pkgs'` module
argument.
5. Pins `home.stateVersion` through `homeStateVersion`.
### Standalone homes
Standalone homes live in [`home-manager/configs/`](../home-manager/configs) and are named
`<user>@<host>` (deploy-rs mangles the `@` to `-at-`). `macsimum` is a home-manager-only
config — an `x86_64-darwin` macOS box with no NixOS side.
No standalone homes are currently wired into the flake: `home-manager/configs/macsimum.nix` is
commented out of `configs`, while `home-manager/configs/castle.nix` is not listed. Consequently,
`nixfiles.config.home-manager.homes` evaluates to `{}`. Most user configuration instead accompanies
NixOS boxes through `my.user.homeConfig`.
+199
View File
@@ -0,0 +1,199 @@
# Deployment and workflows
How boxes get deployed, the devshell commands that drive everyday work, secrets, dev VMs, and
CI.
## deploy-rs
### Rendering
The top-level [`deploy-rs.nix`](../deploy-rs.nix) renders the flake's `deploy` output:
- It collects `nixos.systems` and `home-manager.homes` (mangling `@` in home names to
`-at-`, since deploy-rs node names can't contain `@`). An assertion rejects name collisions
between systems and homes.
- A system/home becomes a deploy node only when `configuration.config.my.deploy.enable` is
true. The node config is the *definitions* of the box's `my.deploy.node` option, re-imported
as a module so deploy-rs's submodule merging still applies.
- `autoRollback` and `magicRollback` are off globally; global `sshOpts` are
`[ "-i" ".keys/deploy.key" ]` (set in [`flake.nix`](../flake.nix)).
- The result is passed through `lib.my.deploy-rs.filterOpts`, which strips nulls so unset
options don't override deploy-rs defaults, and exposed as `deploy = deploy-rs.rendered`.
### Per-box nodes
The shared [`nixos/modules/deploy-rs.nix`](../nixos/modules/deploy-rs.nix) module provides
`my.deploy`:
- `my.deploy.enable` defaults to true, but is **automatically forced off for dev VMs and
NixOS containers** (`my.build.isDevVM` / `boot.isContainer` — containers are deployed
through their host instead, see below). The installer also disables it explicitly.
- Node defaults: `hostname` = the box's FQDN, `sshUser = "deploy"`, `user = "root"`,
`sudo = "doas -u"` (or `sudo -u`), `sshOpts` = the box's first OpenSSH port. The module
creates the `deploy` system user (bash shell, wheel, keys from `my.deploy.authorizedKeys`,
which defaults to `.keys/deploy.pub`).
Generated profiles deploy in this order:
1. **`system`** activates `config.system.build.toplevel` with `switch-to-configuration switch`,
applies the `/tmp` cwd and systemd-boot `loader.conf` workarounds, then prunes old generations.
`keepGenerations` controls how much history is retained; zero disables pruning.
2. **`container-<name>`** activates each `my.containers.instances` entry into
`/nix/var/nix/profiles/per-container/<name>/system`. With `hotReload` (the default), it reloads
`systemd-nspawn@<name>` and restarts only a stopped container or one still running the dummy
init; otherwise it restarts the unit. The same generation cleanup applies.
### Usage
- `deploy .#<host>` — the devshell's `deploy` is a wrapper that adds `--skip-checks`. Node
names are the system names (`deploy .#git`).
- `deploy-multi <hosts...>` — loops `deploy` over several nodes (extra args via `$O`).
- `deploy --boot .#<host>` — stages the config as the boot default **without** live-switching
(deploy-rs's `--boot` maps to the custom activation's `boot` phase, which also does the
generation cleanup). Use it when a live `switch` would cut the box off mid-change (e.g. a
router WAN rework), then reboot to cut over.
- `nix flake check` includes deploy-rs's own `deployChecks` for the whole `deploy` attrset.
#### `ssh-machine`
`ssh-machine <name> [cmd]` resolves `user@host` and the merged global/node `sshOpts` from
`.#deploy`, using the same `my.deploy.enable` gate and `@``-at-` mangling as `deploy`, then runs
`ssh`. Boxes default to `fish`, so pipe multi-statement remote scripts through `bash`, for example
`ssh-machine <name> bash -s < script.sh`. If a flaky agent stalls public-key authentication, use
`SSH_AUTH_SOCK= ssh-machine …`.
## Devshell commands
The repo ships a `numtide/devshell` ([`devshell/`](../devshell), entered via `direnv`). Run a
command with no arguments for its help. From
[`devshell/commands.nix`](../devshell/commands.nix):
| Command | What it does |
|---|---|
| `check-system <host> [nix args]` | Evaluates `.#nixosConfigurations."<host>".config.system.build.toplevel.drvPath` — catches module/option errors without building. Prefer this to validate a config change. |
| `build-system <host> [nix args]` | Builds the system's `toplevel` (extra args pass through to `nix build`). |
| `build-n-switch <args>` | `doas nixos-rebuild --flake .` (adds the repo as a git `safe.directory` for root first). |
| `build-home <name> [nix args]` | Builds `.#homeConfigurations."<name>".activationPackage`. |
| `home-switch [args]` | `home-manager switch --flake .`. |
| `deploy [args]` | `deploy-rs --skip-checks` (wrapper package in `devshell/default.nix`). |
| `deploy-multi <nodes...>` | Deploys several nodes in sequence. |
| `ssh-machine <name> [cmd]` | SSH to a system or home by name, resolving target/options from its deploy-rs node (see above). |
| `run-vm <host>` | Boots a system as a dev VM: installs `.keys/dev.key` into a temp `xchg/`, then `nix run`s `config.my.buildAs.devVM`. |
| `build-iso <host>` | Builds `config.my.buildAs.iso`. |
| `build-kexec <host>` | Builds `config.my.buildAs.kexecTree`. |
| `build-netboot <host>` | Builds `config.my.buildAs.netbootTree`. |
| `ragenix [args]` | `ragenix --identity .keys/dev.key` (see [Secrets](#secrets)). |
| `repl` | `nix repl .#`. |
| `fmt [args]` | `nixpkgs-fmt` (the canonical formatter). |
| `update-nixpkgs` | `nix flake update nixpkgs-{unstable,stable,mine,mine-stable}`. |
| `update-home-manager` | `nix flake update home-manager-{unstable,stable}`. |
| `update-installer` | Force-pushes the `installer` tag to trigger the installer release workflow. |
| `home-link` / `home-unlink` | Symlink (or remove) this `flake.nix` at `~/.config/home-manager/flake.nix` for standalone `home-manager` use. |
| `qemu-genmac` | Prints a random QEMU-suitable MAC (`52:54:00:xx:xx:xx`). |
| `ssh-get-ed25519 <host>` | Prints a host's ed25519 pubkey via `ssh-keyscan`. |
| `json2nix` | Converts JSON on stdin to formatted Nix. |
From [`devshell/install.nix`](../devshell/install.nix) (driven by `$INSTALLER`, the address of
a running custom installer, SSHing as root with `.keys/deploy.key`):
| Command | What it does |
|---|---|
| `installer-shell [cmd]` | Runs a command (default: a shell) inside the installer. |
| `do-install [--no-bootloader] [--no-substitute] <system>` | Builds the system's `toplevel`, `nix copy`s it into the installer's target store, sets the system profile, and activates it with `switch-to-configuration boot` (with `NIXOS_INSTALL_BOOTLOADER=1` unless `--no-bootloader`). |
From [`devshell/vm-tasks.nix`](../devshell/vm-tasks.nix) (remote VM consoles; they forward the
VM's unix sockets from `/run/vms/<vm>/` on `<host>` over SSH):
| Command | What it does |
|---|---|
| `vm-tty <host> <vm>` | Serial TTY of a VM in `minicom`. |
| `vm-monitor <host> <vm>` | QEMU monitor socket in `minicom`. |
| `vm-viewer <host> <vm>` | SPICE display in `virt-viewer` (not on Darwin). |
## Secrets
Secrets are age-encrypted files in [`secrets/`](../secrets), managed with **ragenix** (a fork
with a rekey flag, from the flake inputs).
### Per-box declarations
Each box declares `my.secrets.key`, the host public key its secrets encrypt to, and
`my.secrets.files.<name>`, whose values merge settings such as `owner` and `mode` into the agenix
secret. At runtime, identity paths come from the box's OpenSSH host keys. On tmproot boxes they are
read from the persistence directory because agenix runs before the persisted keys would otherwise
be available.
### Recipient rules
[`secrets.nix`](../secrets.nix) is generated from every system's `my.secrets.files` and `key`.
Each recipient list also includes `.keys/dev.pub`, so the development key can open every secret.
Run `ragenix -r` after adding a box or secret to re-key the files.
### Local keys and editing
The `ragenix` devshell command wraps `ragenix --identity .keys/dev.key`. The `.keys/` directory
contains that development key, the deploy key authorized for every box's `deploy` user, and other
keys referenced by `lib.my.c.sshKeyFiles`; it is required for editing secrets, deploying and running
development VMs.
## Dev VMs
Any system can be built as a throwaway QEMU VM via `config.my.buildAs.devVM` (the `build`
module extends the config with `qemu-vm.nix` and sets `my.build.isDevVM`). `run-vm <host>`
creates a temp dir, installs `.keys/dev.key` as `xchg/dev.key`, and runs the VM; inside, the
`secrets` module switches `age.identityPaths` to that dev key (`my.secrets.vmKeyPath`,
default `/tmp/xchg/dev.key`), so dev VMs can decrypt the boxes' secrets without the real host
keys. Dev VMs also get DHCP on `eth0`, an SSH port forward (host 2222 → guest 22), and are
automatically excluded from deploy targets.
## CI
GitHub/Gitea Actions workflows live in [`.gitea/workflows/`](../.gitea/workflows).
### `ci.yaml`
On pushes to `master`, this runs `nix flake check --no-build`, then builds every attribute of
`.#ci.x86_64-linux`: systems as `system-<name>`, homes as `home-<name>` (with `@` changed to
`-at-`), packages as `package-<name>`, and the development `shell`. Each result is pushed to the
Harmonia cache with [`ci/push-to-cache.sh`](../ci/push-to-cache.sh).
It then builds `.#ciDrv.x86_64-linux`, a `linkFarm` of all CI attributes, and pushes it with
`UPDATE_PROFILE=1`. That updates the `nixfiles` profile on the cache box and collects old paths
according to the workflow's retention setting. The SSH store uses `/var/lib/harmonia`,
`HARMONIA_SSH_KEY`, and pinned `ci/known_hosts`; clients use `https://nix-cache.nul.ie` through
`lib.my.c.nix.cache`.
### `installer.yaml`
Pushing the `installer` tag (refreshed by `update-installer`) builds `my.buildAs.iso` and
`my.buildAs.netbootArchive`, then attaches both to a release.
### `update-docs.yaml`
On pushes to the docs branch, excluding its own commits, this runs
`nix run .#update-docs-assignments` and `nix run .#update-docs-options` and commits changed outputs
as `docs: update generated tables`.
### The docs generators
Both are registered in [`pkgs/default.nix`](../pkgs/default.nix) (`writeShellScriptBin`s wrapping
Python scripts under [`ci/`](../ci)). They leave the worktree unchanged when their output is current;
the workflow stages `docs/` and uses `git diff --cached --quiet` to decide whether to commit.
`update-docs-assignments` ([`ci/update-docs-assignments.py`](../ci/update-docs-assignments.py))
evaluates `.#nixfiles.config.nixos.allAssignments` to JSON and rewrites the consolidated
[`Box assignments`](networking.md#box-assignments) tables in `docs/networking.md` — one table
per site (`colony` / `home` / `remote`), each between a `<!-- assignments: <site> -->` marker
and a closing `<!-- assignments-end -->` line. Boxes are grouped by site from their assignment
domain, and each `Box` cell links to that box's page when one exists. Hand-written text in the
**Notes** column is preserved across runs (keyed by box + assignment), so notes survive
regeneration. Individual box pages don't carry tables; they link to the consolidated section.
`update-docs-options` ([`ci/update-docs-options.py`](../ci/update-docs-options.py)) builds the
`nixos.optionsDoc` output (declared in [`nixos/default.nix`](../nixos/default.nix)) — a
`nixosOptionsDoc` JSON dump of the custom `my.*` options (declared with `mkOpt'` / `mkBoolOpt'` in
`nixos/modules/`). It's evaluated against a **minimal synthetic system**, since the shared modules
apply to every box, so defaults don't pick up a real host's values. The renderer writes
[`docs/reference/nixos-options.md`](reference/nixos-options.md), one table per module file. The
whole file is generated; edit the option descriptions in the modules, not the reference. The
internal `asX` build-target options are marked `internal = true` so they're excluded.
+41
View File
@@ -0,0 +1,41 @@
# installer
The custom NixOS installer image used to bootstrap new boxes.
- **Source:** [`nixos/installer.nix`](../../nixos/installer.nix)
- **Host:** — (a build target, not a deployed box)
## Role
- Defines `nixos.systems.installer`, a minimal server system rendered as a bootable ISO via
`config.my.buildAs.iso` (`my.asISO`); the same base can also be built as a kexec or netboot
tree.
- Build it with the devshell commands: `build-iso installer` (or `build-kexec installer` /
`build-netboot installer`). The `update-installer` command force-tags `installer` to
trigger a release rebuild in CI.
## Image contents
- Broad hardware support (`my.build.allHardware` pulls in the nixpkgs all-hardware profile);
EFI- and USB-bootable, zstd-compressed squashfs. Volume ID
`jackos-<release>-<arch>`, menu label "/dev/player0 Installer", image base name
`jackos-installer`.
- Root SSH with the deploy key authorized (`PermitRootLogin prohibit-password`); a random
`installer-<hex>` hostname is set at boot.
- `INSTALL_ROOT=/mnt` in the session environment, plus a `show-hw-config` alias wrapping
`nixos-generate-config --show-hardware-config --root $INSTALL_ROOT`.
- NixOS documentation enabled, `wpa_supplicant` available but not started, GC and memory-overcommit
tuning for low-memory targets, LVM thin and NFS support.
- No regular user (`my.user.enable = false`), no tmpfs-root management, no NAT, and not a
deploy target (`my.deploy.enable = false`).
## Installing a box
The devshell's installer commands ([`devshell/install.nix`](../../devshell/install.nix)) drive
an install over SSH against a booted installer reachable at `$INSTALLER`:
- `installer-shell` — get a shell on the installer.
- `do-install <system>` — builds the system's toplevel, `nix copy`s the closure to the
installer's `$INSTALL_ROOT` remote store, sets the system profile, touches `/etc/NIXOS`,
and runs `switch-to-configuration boot` with `NIXOS_INSTALL_BOOTLOADER=1` (skip the
bootloader with `--no-bootloader`, skip substitution with `--no-substitute`).