Your first fleet
Prerequisite: the Foundations chapters (What Nix is and why it matters through NixOS and the module system) and Secrets with sops-nix, Remote install with nixos-anywhere, and Deploying with deploy-rs. Also a Linux target you can wipe (a $5 VPS with root SSH access is ideal).
Outcome: a git repo describing your fleet; a real NixOS host you installed remotely; a deploy round-trip.
This is the payoff chapter. Everything so far has been context. Here you actually run the tools.
Prerequisites checklist
- [x] Nix installed with flakes enabled (Install Nix and enable flakes). Verify:
nix --versionandcat ~/.config/nix/nix.conf | grep experimental. - [x] A public SSH key pair on your laptop:
ls ~/.ssh/*.pub. If none,ssh-keygen -t ed25519. - [x] A Linux target you can wipe. Anything reachable via SSH as root: a fresh Hetzner/DigitalOcean/OVH VPS, an old ThinkPad, a Raspberry Pi. Get its IP.
- [ ] An age key (optional; wizard offers to generate one). If you
already have
~/.config/sops/age/keys.txt, we will use it.
Step 1: run the wizard
From any directory (the wizard creates a new subdirectory for the fleet):
$ nix run github:reflection-dev/nixops -- new my-fleet
nix run fetches this repo (first time) and executes its default
app, which is the new scaffolder. The -- after nixops
separates arguments to nix run itself from arguments to the
wrapped program (Flakes covered this).
The wizard asks:
- Description. Freeform. Defaults to
my-fleet. - SSH key. Auto-detected from
~/.ssh/*.pub. Pick one; it becomes root’sauthorized_keyson every host in the fleet. Add more later by editingflake.nix. - age recipient. Auto-detected from
~/.config/sops/age/keys.txt. If none, offers to generate. This is the admin key – your laptop’s ability to decrypt secrets. - Admin recipient name. The alias used in
.sops.yaml(defaultadmin_${USER}). Cosmetic; pick something meaningful if you will have multiple admins. git init? Recommended yes; the wizard makes a first commit so the tree is committed (Flakes explains why this matters forpath:inputs).
When it finishes:
created ./my-fleet/
Next:
cd my-fleet
nix develop
add-host
install-host
Step 2: inspect what was scaffolded
$ cd my-fleet
$ ls -a
.gitignore .sops.yaml flake.nix hosts.nix README.md
Five files. Open each in your editor. What you see:
flake.nix– inputs (nixops, and follows for nixpkgs/sops-nix/deploy-rs) and outputs (nixosConfigurations,deploy.nodes,checks,devShells). ThesshKeys = [ ... ]list holds the key you picked. This is the whole flake for the fleet.hosts.nix– currently{ }. Every host you add lands here as a key..sops.yaml– one entry for your admin recipient, emptycreation_rules.install-hostgrows both when hosts are added..gitignore– ignoresresultsymlinks,.direnv/, andsecrets/*/(where install-host stashes temporary host-key material).README.md– a one-page reminder of the ops commands.
Anatomy of an instance repo walks each file in detail. For now, glance and move on.
Step 3: enter the devShell
$ nix develop
nixops devShell -- 0 host(s) in inventory
You are now in a shell where ssh, add-host, install-host,
update-secrets, deploy, plus sops, age, ssh-to-age,
nixos-anywhere, jq, gum are on PATH. Confirm:
$ which ssh
/nix/store/...-ssh/bin/ssh
$ which install-host
/nix/store/...-install-host/bin/install-host
Each is a pkgs.writeShellApplication from lib/mkDevShell.nix,
wrapping the corresponding script in scripts/.
The shell also generated an ssh_config that maps your host names
(none yet) to their IPs from hosts.nix.
Tip: using direnv with the use flake hook makes entering
the devShell automatic when you cd into the repo. See
direnv + nix-direnv.
Step 4: add a host to the inventory
$ add-host web-1
IP address: 1.2.3.4
add-host:
-
checks
web-1is not already inhosts.nix; -
creates
hosts/web-1/hardware-configuration.nixas a stub (empty module – the real one comes back frominstall-host); -
appends an entry to
hosts.nix:web-1 = { ip = "1.2.3.4"; modules = [ ./hosts/web-1 ]; };
Nothing has touched the target yet. hosts.nix is data; the wizard
scaffolds the directory but does not install anything.
Step 5: install the host
Now the real thing. Make sure you have root SSH access to the target from your workstation:
$ ssh root@1.2.3.4 uname -a
Linux debian-12 6.1.0-... x86_64 GNU/Linux
Then, from inside the devShell in your fleet repo:
$ install-host web-1
This is going to do everything you learned in Secrets with sops-nix, Remote install with nixos-anywhere, and Deploying with deploy-rs, in order. Watch the output; every line maps to something you have seen:
- IP lookup + NixOS safety check. If the target is already
NixOS, refuses without
--force. .sops.yamldoesn’t need updates (since this scaffold has nosops.secrets.*declared yet). If your host module did declare them,install-hostwould generate a host key, add the recipient to.sops.yaml, and prompt for values.nixos-anywhere --flake .#web-1 root@1.2.3.4. Kexec, wipe, install, reboot. Watch the SSH connection drop once and come back.
Coffee break. First deploy on a small VPS is 5-10 minutes.
When it finishes, the host has been reinstalled with your NixOS configuration. Verify:
$ ssh web-1
[root@web-1:~]# nixos-version
25.11.20260315.abcdef1 (Vicuna)
ssh web-1 works because the devShell’s generated ssh_config
resolves the name to its IP. nixos-version confirms you are on
NixOS.
Step 6: change something and deploy
You have a running NixOS host, installed from a fresh Debian, with sshd hardened, a firewall, and root’s authorized_keys set to your laptop key. What’s next?
Let’s open the firewall for HTTP. Edit hosts.nix:
web-1 = {
ip = "1.2.3.4";
modules = [ ./hosts/web-1 ];
nixops.firewall.allowedTCPPorts = [ 80 443 ]; # <-- add this
};
Any field on the inventory entry that is not ip, system, or
modules becomes an inline module fragment for that host (see
NixOS and the module system for the specialArgs + inline-module trick in
mkNixosConfigs). So this line is equivalent to writing a module
that sets nixops.firewall.allowedTCPPorts = [ 80 443 ]; – which
modules/firewall.nix picks up.
Now deploy:
$ deploy web-1
deploy-rs:
- builds the new closure on your laptop;
- copies it to
web-1; - activates it (systemd reloads, iptables is updated);
- pings the target from your laptop;
- if the ping fails, rolls back.
Verify:
$ ssh web-1
[root@web-1:~]# iptables -L -n | grep -E "80|443"
Step 7: understand what you now have
git statuson the fleet repo shows the changes tohosts.nix. Commit them: the git history of this repo is now the change log of your fleet.nix flake showlists every output: your onenixosConfigurations.web-1, onedeploy.nodes.web-1, one devShell, etc.- Rollback: if magic-rollback did not fire on its own, the
clean path is
git checkout <good-commit> && deploy web-1 && git checkout main. As a last resort when the host is unreachable over SSH, console in and runnixos-rebuild switch --rollback. - Adding a second host: repeat Step 4-5 with a new name and a
new IP.
deploy(no arg) deploys both concurrently.
The whole loop, once you know it
edit *.nix # change desired state
git add + commit # snapshot it
deploy [name] # ship it
If a change involves secrets: update-secrets [name] before
deploy. If a change involves a new host: add-host <name> +
install-host <name>. That is the entire operational surface.
What next
Anatomy of an instance repo walks every file in the instance repo in detail so you know what to change when. Day-two operations covers the day-two loop – key rotation, adding admins, splitting hosts across environments, diagnosing a failed deploy. Writing host-specific modules shows how to write your own host-specific modules (adding a real service, a disko layout).
Next: Anatomy of an instance repo
References for this chapter
- Zero to Nix – a first NixOS system – friendly walkthrough of building a NixOS system from scratch, complements this chapter well.
- NixOS Manual – installing NixOS
- NixOS Discourse – getting started category
- Your own fleet’s
README.md– the wizard scaffolded a short-form version of this chapter into it.