Adding a New Host#
This guide walks through adding a new NixOS host from scratch — from creating the config files to a working, post-install system with secrets access.
Prerequisites#
Before starting, make sure you have:
- The devshell active (
nix developor direnv): gives yousops,gum,just, etc. - Your personal age key at
~/.config/sops/age/keys.txt(runjust sops-initif missing) - Your SSH public key committed under
modules/users/<you>/keys/ - Write access to
secrets.yaml(verify withjust preflight)
Quick environment check:
just preflight
Step 1 — Scaffold the host directory#
Use the scaffold recipe to create the host directory and see the hosts/default.nix
snippet you’ll need to add:
just new-host <name> # x86_64 NixOS (default)
just new-host <name> aarch64 nixos # aarch64 NixOS (e.g. RPi)
just new-host <name> aarch64 darwin # Apple Silicon Mac
This creates hosts/<arch>-<class>/<name>/default.nix with a minimal template and
prints the entry to paste into hosts/default.nix.
If you prefer to do it manually, create the directory:
hosts/
└── x86_64-nixos/
└── <name>/
└── default.nix
Step 2 — Write the host config#
Edit hosts/<arch>-<class>/<name>/default.nix. A minimal NixOS host looks like:
{ inputs, lib, config, pkgs, ... }: {
imports = [
# Hardware quirks (optional — pick from nixos-hardware if applicable)
# inputs.hardware.nixosModules.common-cpu-intel
# Disk layout (required — define a disko config or import one)
# ./disko.nix
# Users on this host
../../../modules/users/tsunami
];
networking.hostName = "<name>";
# Disk / filesystem config here (see hosts/x86_64-nixos/mokou/ for a full example)
tsunaminoai = {
tailscale.enable = true;
# Uncomment to enable Borg backups (see Step 5):
# borg = { enable = true; repo = "<borgwarehouse-repo-id>"; };
# Builder/cache enablement is driven by the host's tags in hosts/default.nix
# (see Step 3), not set here.
};
system.stateVersion = "26.05";
}
This matches what
just new-hostscaffolds. The generated file only setstailscale.enable = trueand leavesborgcommented out.
See hosts/x86_64-nixos/mokou/default.nix for a full desktop example, or
hosts/x86_64-nixos/ereshkigal/ for a server example.
Step 3 — Register in hosts/default.nix#
Add an entry inside the hosts = { ... } attrset in hosts/default.nix.
The just new-host command prints the snippet for you; paste it in alphabetical order
(the file uses # keep-sorted markers):
<name> = {
path = ./<arch>-<class>/<name>;
arch = "<arch>"; # x86_64 (default) or aarch64
class = "<class>"; # nixos (default), darwin, rpi, wsl, catalina
system = "<arch>-linux"; # use <arch>-darwin for darwin/catalina hosts
tags = [ ]; # see Tags section below
};
The class selects which module wiring a host gets. nixos and darwin are the
base classes; rpi, wsl, and catalina are extra classes mapped back to a base
class by additionalClasses in hosts/default.nix (rpi → nixos, wsl →
nixos, catalina → darwin). Setting class = "rpi" is what selects the
Raspberry Pi module wiring — it is a class value, not a tag.
Available tags:
Only builder and cache are functional tags — they are consumed by perTag in
modules/flake/nix/default.nix. All other tags are purely descriptive labels with
no automatic behaviour; the features they describe are enabled by setting the
relevant tsunaminoai.* options and module imports in the host’s own default.nix.
| Tag | Effect |
|---|---|
builder |
Sets tsunaminoai.nix.builds.enable = true (registers host as a Nix remote builder) |
cache |
Sets tsunaminoai.nix.isCache = true (hosts a binary cache other hosts use) |
desktop |
Descriptive label only — enable KDE/Wayland/Pipewire via tsunaminoai.* options yourself |
server |
Descriptive label only — no automatic behaviour |
laptop |
Descriptive label only — no automatic behaviour |
media-acquisition |
Descriptive label only — the *arr stack is enabled via tsunaminoai.servarr.* options (Tdarr is currently force-disabled, lib.mkIf false, because it hangs on reboot) |
The builder / cache tags act through two paths: perTag in
modules/flake/nix/default.nix flips the per-host tsunaminoai.nix options above,
while hosts/default.nix builds discoveredBuilderHosts / discoveredCacheHosts
(lists of host names) as special args so other hosts can wire themselves into the
build and caching mesh.
Step 4 — Verify the flake evaluates#
nix flake show 2>&1 | grep <name>
Fix any evaluation errors before proceeding.
Step 5 — Generate and register a Borg backup key (if using Borg)#
If the host will use Borg backups (tsunaminoai.borg.enable = true):
just borg-generate-secrets <name>
This generates an ed25519 keypair and passphrase, writes them to secrets.yaml, and
prints the public key. Register the public key in your BorgWarehouse instance to get
the repo ID, then put that ID in the host config (tsunaminoai.borg.repo).
Step 6 — First boot: install the system#
Option A — Interactive installer ISO (local machine)#
- Build and write the installer ISO:
nix build .#nixosConfigurations.bootable-iso.config.system.build.isoImage
# Flash result/iso/*.iso to a USB drive
- Boot the target machine from the USB. The flake is included in the ISO at
/etc/nixos-flake. Run the interactive installer:
install.sh
It will prompt for host selection, confirm disk destruction, run disko, and call
nixos-install.
Option B — nixos-anywhere (remote, over SSH)#
From a machine in the devshell:
nix run github:nix-community/nixos-anywhere -- \
--flake .#<name> \
root@<target-ip>
Requires the target to have an SSH server running (minimal NixOS live ISO or existing Linux install with root SSH access).
Step 7 — Register the host’s SOPS age key#
After the first boot, the host has an SSH host key at /etc/ssh/ssh_host_ed25519_key.
sops-nix derives the host’s age key from this.
On the new host, convert the host key to age format:
nix-shell -p ssh-to-age --run \
'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'
# → age1...
Back in the flake repo, add the age key to .sops.yaml:
# In the hosts: section:
- &<name> age1<output-from-above>
# In the creation_rules → secrets.yaml key_groups → age: section:
- *<name>
Then update the encrypted file so it’s re-keyed for the new host:
sops updatekeys secrets.yaml
git add .sops.yaml secrets.yaml
git commit -m "feat: add sops age key for <name>"
Save the host key in the repo for deploy-rs:
just get-host-key <name>
git add modules/nixos/security/pubkeys/<name>.pub
git commit -m "chore: save host key for <name>"
Step 8 — Second nixos-install (with secrets)#
Push the commits from Step 7, then on the target machine re-run the install so sops-nix can decrypt secrets during activation:
nixos-install --flake /etc/nixos-flake#<name>
reboot
Or deploy remotely after the system is up:
nix run .#deploy -- .#<name>
Step 9 — Post-install checklist#
- Confirm Tailscale joined the mesh:
tailscale status. Withtsunaminoai.tailscale.enable = true(set by the scaffold template) thetailscale-autoconnectservice runstailscale up --auth-key=...automatically on first boot from thetailscale/auth-keysops secret; only runtailscale up --auth-key <key>manually as a fallback if that secret is missing. - Verify sops secrets decrypted:
ls /run/secrets/ - Verify Borg backup connectivity:
borgmatic check - Verify deploy-rs:
nix run .#deploy -- .#<name> --dry-activate - Add host to
tsunaminoai.deploy.monitoredHostsin the deploy node (e.g.mokou) if you want it in the deploy health dashboard (tsunaminoai.deploy, default port 8420)
Adding a collaborator user#
To give a new person access to the flake:
- Create
modules/users/<username>/(copymodules/users/bcraton/as a minimal template) - Add their SSH public key to
modules/users/<username>/keys/ - Generate their personal age key on their machine (
just sops-init) and add it to.sops.yamlunder theusers:section and to thesecrets.yamlcreation rules - Run
sops updatekeys secrets.yamland push - Import
../../../modules/users/<username>in any hostdefault.nixwhere they need access