Built on NixOS. A single repo describes your set of servers as plain data: hostnames, secrets, disks, and per-host configuration. Interactive wizards add hosts, provision secrets, install NixOS on any SSH-reachable Linux, and roll fleet-wide updates out with automatic rollback, plus an ephemeral operator VM you can throw away.
New to NixOS? The zero-to-fleet tutorial starts from "what even is this" and ends with you running a real fleet. No prior functional-programming or DevOps background required.
- Quick start
- Ephemeral operator VM
- Instance repo layout
- devShell commands
- Flake outputs
- Base modules
- Bootstrap flow
- New to Nix?
- Non-goals
- Contributing
- License
Requires Nix with flakes enabled.
nix run github:reflection-dev/nixops -- new my-fleet
cd my-fleet
nix develop
The wizard prompts for description, SSH keys (auto-detected from
~/.ssh/*.pub), age recipient (auto-detected from
~/.config/sops/age/keys.txt), optionally git inits the repo, and
optionally scaffolds a first host through add-host.
Bare-bones alternative for those who prefer to edit placeholders by hand:
nix flake init -t github:reflection-dev/nixops
For dry-runs, demos, or driving a fleet from a machine you do not want to mix
with your real ~/.ssh / ~/.config/sops/age, boot a throwaway NixOS
workstation:
nix run github:reflection-dev/nixops#opsvm
Optional name (becomes the guest hostname and the state-dir slug):
nix run github:reflection-dev/nixops#opsvm -- my-fleet
# equivalently: OPSVM_NAME=my-fleet nix run github:reflection-dev/nixops#opsvm
State lives on the host under ${XDG_STATE_HOME:-~/.local/state}/nixops-opsvm/<name>/:
<state>/
|-- opsvm.qcow2 persistent VM disk (rm to reset)
|-- ssh/ mounted into guest at /home/ops/.ssh
`-- sops-age/ mounted into guest at /home/ops/.config/sops/age
Notes:
- QEMU is bundled through Nix; the host only needs
/dev/kvm. - Serial console autologins as
ops(unprivileged, passwordless sudo). Exit QEMU withCtrl+A, thenx. - Different names get separate state dirs and disks, so you can juggle several isolated workstations from one command.
- No inbound sshd; the VM initiates outbound ssh only (
nixos-anywhere,install-host, ...).
Five files. Everything else grows as hosts are added.
my-fleet/
|-- flake.nix inputs + outputs (nixosConfigurations, deploy.nodes, devShell)
|-- hosts.nix plain-data inventory: { <name> = { ip; modules; ... }; ... }
|-- .sops.yaml age recipients (admins + per-host)
|-- .gitignore
`-- README.md
Shared SSH keys live inline in flake.nix (one list, every host). Per-host
NixOS modules live under hosts/<name>/ (default.nix + disko.nix +
hardware-configuration.nix, all scaffolded by add-host) and are pulled
in via the modules = [ ./hosts/<name> ]; field on the inventory entry.
See Host files for what each file does and
how to swap in your own disk layout.
nix develop on the instance repo hands the operator:
| command | what it does |
|---|---|
ssh <name> |
ssh via a config generated from the inventory (Host <name> ...) |
add-host <name> |
prompts for IP + disk layout preset, scaffolds hosts/<name>/, appends to hosts.nix |
install-host <name> |
nixos-anywhere (generates hardware-configuration.nix) + sops recipient inject + interactive secret prompts |
update-secrets [name] |
interactively fill any sops.secrets.* not yet set |
set-secret <host> <key> <file> |
non-interactive: write one secret from a file (or - for stdin) |
deploy [name] |
deploy-rs wrapper: all nodes, or one by name |
Plus in PATH: sops, age, ssh-to-age, deploy-rs, nixos-anywhere,
jq, gum. All interactive prompts use gum, not plain read.
nixops.nixosModules.default # aggregator: sops-nix + all base modules
nixops.nixosModules.{ssh, sops, users, nix-defaults, firewall}
nixops.nixosModules.opsWorkstation # ops user + toolchain (opt-in, workstation role)
nixops.lib.mkNixosConfigs { hosts; sshKeys; }
nixops.lib.mkDeploy nixosConfigurations
nixops.lib.mkDevShell { hosts; system; extraPackages? }
nixops.templates.default # for `nix flake init -t`
nixops.packages.<system>.opsvm # nixos-vm: opsWorkstation + qemu-vm.nix
nixops.apps.<system>.{new, default} # the wizard
nixops.apps.<system>.opsvm # ephemeral workstation launcher
Every module is toggle-driven (nixops.<name>.enable, default true for the
sane cases). Any host can opt out of any individual piece.
| module | purpose |
|---|---|
nix-defaults |
flakes, weekly GC (30d retention), UTC, UTF-8 locale, system.stateVersion, base CLI toolbox |
ssh |
hardened sshd; root allowed by key only, no passwords, no keyboard-interactive |
sops |
sops.age.sshKeyPaths = [ /etc/ssh/ssh_host_ed25519_key ] |
users |
seeds root's authorized_keys from the shared sshKeys list |
firewall |
allowlist mode; extra ports via nixops.firewall.allowedTCP/UDPPorts |
- Have an age key at
~/.config/sops/age/keys.txt(or let the wizard generate one). Its recipient goes into.sops.yamlas&admin_<name>. add-host <name>scaffolds a host entry and prompts for a disk layout preset (currentlyhetzner-vm, orcustomfor hand-writtendisko.nix).install-host <name>runsnixos-anywhere:- generates the target's
ssh_host_ed25519_keylocally, - derives its age recipient (
ssh-to-age), - appends
&<name>to.sops.yaml+ acreation_ruleforsecrets/<name>.yaml, - prompts for any missing
sops.secrets.*, - generates
hosts/<name>/hardware-configuration.nixfrom live hardware (nixos-generate-configon the target), - kexecs NixOS with the host key as
--extra-files, - removes the local copy of the private key on success.
- generates the target's
- Subsequent updates:
deploy <name>(ordeployfor the whole fleet).
The zero-to-fleet tutorial under docs/
is written for ops folks who have never touched Nix, NixOS, or flakes. It
covers the language, the module system, sops-nix, nixos-anywhere, and
deploy-rs, ending with a walkthrough of installing and operating a real
fleet with this repo. Every chapter links to the canonical Nix docs
(nix.dev, NixOS Manual, Zero to Nix, NixOS Wiki, Discourse) for deeper
dives.
- AI-specific tooling (agents, coding assistants, model hosts). That
layer lives in reflection-dev/castle,
which will build on top of
nixops. - A catalog of disko recipes.
nixopsships one preset per common shape (currently justhetzner-vm, a single-disk UEFI VM) plus an always-presentcustomoption. It is not aiming to become a zoo of provider-specific layouts; anything past the trivial single-disk case is acustomdisko.nix or a preset PR. See Host files. - Managed service modules (Postgres, Redis, ...). Add these in the instance or in a purpose-specific library on top.
- Multi-cloud abstractions.
install-hostworks over any SSH target reachable bynixos-anywhere; provider-specific modules can land later as opt-in extras.
Issues and PRs welcome. See CONTRIBUTING.md for the development setup, the module/script conventions, and the PR checklist. Be kind (Code of Conduct); report vulnerabilities privately per SECURITY.md.
MIT (c) 2026 Andy Smith.