Blog
The Complete NixOS Deployment Stack: Install Once, Manage Forever
Installing and Managing NixOS using nixos-anywhere, Disko, and Colmena
Article info
When we first started managing NixOS machines, the install process felt disconnected from how we managed things later. We would install manually, take notes somewhere, and hope the next install would go the same way. It rarely did.
The stack we are going to walk through today changes that. With nixos-anywhere, Disko, and Colmena, all working together inside a Nix flake, you describe a machine once and the same description handles both the initial install and every change you make afterward. The tooling takes care of the rest.
This post goes through each tool, explains what it does, and shows you how they fit together into a workflow you can follow from the beginning.
A quick note on flakes
Before getting into the tools, it helps to understand what a Nix flake is doing here.
A flake is a way to organize Nix code into a reproducible, self-contained package. It has inputs — like specific versions of nixpkgs, disko, or colmena — and outputs, which are the things we want to build or use. For our purposes, the main output is a set of nixosConfigurations, one per machine.
Every tool in this stack reads from the same flake. That single source of truth is what makes the whole workflow consistent.
If you have not used flakes before, you need to make sure they are enabled. Check if your Nix setup supports them by running:
nix flake --help If Nix tells you flakes are an experimental feature, you can enable them by adding this to your Nix configuration:
experimental-features = nix-command flakes What each tool is responsible for
It is easier to understand this stack when you see each tool as having one clear job.
nixos-anywhere
nixos-anywhere installs NixOS on a remote machine over SSH. You do not need physical access to the machine. You do not need to boot from a USB drive. You point it at an IP address, give it your flake, and it handles the rest.
What it does:
- Connects to the target machine over SSH
- Uses
kexecto boot into a NixOS installer environment if the machine is not already running NixOS - Runs
Diskoto partition and format the disks - Installs NixOS using the configuration in your flake
- Optionally generates a
hardware-configuration.nixfor the target machine
Important: nixos-anywhere completely overwrites the target machine. Never point it at a machine with data you want to keep.
Disko
NixOS lets you describe almost everything as code, but disk partitioning has traditionally been a manual step. Disko fills that gap.
You write a disko.nix file that describes how the disk should be laid out — partitions, filesystems, mount points — and Disko makes it so. This configuration becomes part of your flake, so every install of that machine will produce the same disk layout every time.
Disko supports most common layouts: GPT, MBR, LVM, LUKS, ext4, btrfs, ZFS, and more.
Colmena
Once a machine is installed, you will keep making changes to it. Maybe you add a new service, update a package, or change a configuration option. Colmena is what handles those day-to-day changes.
Colmena is a stateless NixOS deployment tool. It reads your flake, builds the new system, and pushes it to one or more machines over SSH. It can deploy to multiple machines in parallel, and you can filter deployments by hostname or tag.
The important thing: Colmena does not need to know how a machine was originally installed. It only needs SSH access and a valid flake.
The mental model
It helps to think about this in three phases:
Day 0 — Prepare the flake. Write your configuration.nix, your disko.nix, and wire them into flake.nix. Add an SSH public key so you can access the machine after install.
Day 1 — Install the machine. Run nixos-anywhere against the target machine’s IP address. It installs NixOS, formats the disk with Disko, and generates a hardware configuration. You commit that hardware config back to the flake.
Day 2 and beyond — Manage changes. Edit your configuration files. Run Colmena to apply the changes. Repeat indefinitely.
The machine is installed once. Everything after that is managed through the same flake.
A simple flake layout for one host
Here is the folder structure we recommend when managing a single host. It is easy to extend to multiple hosts later.
my-infra/
├── flake.nix
├── flake.lock
└── hosts/
└── my-server/
├── configuration.nix
├── disko.nix
└── hardware-configuration.nix Each piece has one purpose:
flake.nix— declares inputs and wires up host configurationshosts/my-server/configuration.nix— the NixOS configuration for that machinehosts/my-server/disko.nix— the disk layout for that machinehosts/my-server/hardware-configuration.nix— generated during the first install bynixos-anywhere, describes the actual hardware
Writing the flake
Here is a minimal flake.nix that supports nixos-anywhere installs, Disko disk layouts, and Colmena deployments for one host:
{
description = "My NixOS infrastructure";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
colmena = {
url = "github:zhaofengli/colmena";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs @ { self, nixpkgs, disko, colmena, ... }: {
nixosConfigurations.my-server = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
disko.nixosModules.disko
./hosts/my-server/configuration.nix
./hosts/my-server/hardware-configuration.nix
];
};
colmena = {
meta = {
nixpkgs = import nixpkgs { system = "x86_64-linux"; };
};
my-server = {
imports = [
disko.nixosModules.disko
./hosts/my-server/configuration.nix
./hosts/my-server/hardware-configuration.nix
];
deployment = {
targetHost = "your-server-ip";
targetUser = "root";
allowLocalDeployment = true;
};
};
};
colmenaHive = colmena.lib.makeHive self.outputs.colmena;
};
} A few things worth noting here:
disko.nixosModules.diskois imported in bothnixosConfigurationsandcolmena. This is what activates the disk layout as part of the NixOS system.colmenaHiveis required by the current Colmena flake integration. It tells Colmena which output to use.- The
hardware-configuration.niximport is there as a placeholder. It will be generated during the first install.
Writing a minimal NixOS configuration
Here is a starting point for configuration.nix. This gives you a bootable machine with SSH access:
{ pkgs, ... }:
{
imports = [ ./disko.nix ];
boot.loader.grub = {
enable = true;
efiSupport = true;
efiInstallAsRemovable = true;
};
networking.hostName = "my-server";
services.openssh = {
enable = true;
settings.PermitRootLogin = "prohibit-password";
};
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAA... your-public-key-here"
];
environment.systemPackages = with pkgs; [ git vim ];
system.stateVersion = "25.05";
} Replace your-public-key-here with your actual SSH public key. You will not be able to log in after install without this.
Describing the disk layout with Disko
Here is a simple disko.nix for a single disk with a GPT partition table, an EFI boot partition, and an ext4 root:
{
disko.devices = {
disk = {
main = {
device = "/dev/sda";
type = "disk";
content = {
type = "gpt";
partitions = {
ESP = {
type = "EF00";
size = "512M";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
};
} Check which disk name your target machine uses before install. You can do this by SSHing into the machine and running lsblk. Adjust /dev/sda to match your actual disk — on some machines it might be /dev/vda or /dev/nvme0n1.
Installing the machine with nixos-anywhere
Before running the install, lock your flake to pin the dependency versions:
nix flake lock Now run nixos-anywhere. You do not need to install it separately — you can run it directly from nixpkgs:
nix run nixpkgs#nixos-anywhere -- \
--generate-hardware-config nixos-generate-config ./hosts/my-server/hardware-configuration.nix \
--flake .#my-server \
--target-host root@your-server-ip What each flag does:
--generate-hardware-config nixos-generate-config ./hosts/...— generateshardware-configuration.nixon the target and saves it locally--flake .#my-server— points to themy-serveroutput in your local flake--target-host root@your-server-ip— the machine to install onto
The process takes a few minutes. When it finishes, you will see:
Installation finished. No error reported. The machine is now running NixOS. It also generated hardware-configuration.nix in your local hosts/my-server/ folder. Commit that file to your repository so future deployments include it.
After install, the machine’s SSH host key has changed. If you connected to it before, remove the old entry from your known_hosts:
ssh-keygen -R your-server-ip Managing changes with Colmena
After the first install, you should not use nixos-anywhere again for regular changes. That is what Colmena is for.
Colmena reads your flake and applies whatever is in the colmena output to the target machines over SSH. The basic command is:
nix run github:zhaofengli/colmena -- apply --on my-server If the target machine is better suited to build the system itself (for example when your local machine is macOS and cross-compilation is awkward), you can tell Colmena to build on the target:
nix run github:zhaofengli/colmena -- apply --build-on-target --on my-server If you have multiple machines, Colmena can deploy to all of them at once, or you can filter by tag:
# Deploy to all hosts
nix run github:zhaofengli/colmena -- apply
# Deploy to a specific host
nix run github:zhaofengli/colmena -- apply --on my-server
# Deploy to hosts with a specific tag
nix run github:zhaofengli/colmena -- apply --on @web Tags are defined in the deployment.tags field per host inside the colmena output. They are useful once you have several machines grouped by role.
Validating before you deploy
Before applying changes to a real machine, you can build the system locally to catch any evaluation errors:
nix build .#nixosConfigurations.my-server.config.system.build.toplevel If the build succeeds, you know the configuration evaluates cleanly. This is not a full test, but it catches typos and missing modules before they reach a production machine.
A complete workflow from the beginning
Here is the full sequence for someone starting from a blank project:
# 1. Create the project structure
mkdir -p my-infra/hosts/my-server
cd my-infra
# 2. Write flake.nix, configuration.nix, and disko.nix
# 3. Lock the flake
nix flake lock
# 4. Install the machine (hardware-configuration.nix will be generated)
nix run nixpkgs#nixos-anywhere -- \
--generate-hardware-config nixos-generate-config ./hosts/my-server/hardware-configuration.nix \
--flake .#my-server \
--target-host root@your-server-ip
# 5. Commit the generated hardware configuration
git add hosts/my-server/hardware-configuration.nix
git commit -m "add hardware configuration for my-server"
# 6. Make changes to configuration.nix as needed
# 7. Apply changes with Colmena
nix run github:zhaofengli/colmena -- apply --on my-server This is the whole loop. Install once, manage forever.
Adding more hosts
When you need a second machine, the pattern is the same. Add a new directory under hosts/, write its configuration and disko files, add it to flake.nix under both nixosConfigurations and colmena, then install it with nixos-anywhere.
If you find yourself sharing configuration between machines — a common SSH setup, shared packages, or firewall rules — pull that into a shared module:
my-infra/
├── flake.nix
├── flake.lock
├── modules/
│ └── common.nix
└── hosts/
├── web-server/
│ ├── configuration.nix
│ ├── disko.nix
│ └── hardware-configuration.nix
└── db-server/
├── configuration.nix
├── disko.nix
└── hardware-configuration.nix Each host imports modules/common.nix for shared settings, and keeps its own specific configuration in its configuration.nix. This keeps things organized without repeating yourself.
How we use this in Semesta
Semesta is our own infrastructure repository. It follows the same layout described above — one directory per host under hosts/, shared logic under modules/nixos/, and a flake.nix that wires everything together.
It currently manages three machines:
vpn— a self-hosted NetBird VPN nodelb01— an nginx load balancer for the Kubernetes APIkube01— a single-node k3s cluster
Each host has its own disko.nix for the disk layout, configuration.nix for the NixOS setup, and hardware-configuration.nix generated during the first install.
The operating pattern is exactly what this post describes:
- First install:
nixos-anywherewith--generate-hardware-configto install and capture hardware details - Day-to-day:
colmena apply --build-on-target --on <host>to apply changes from the flake
If you are curious about what a working multi-host setup looks like, Semesta is a small and readable example.
Looking ahead
This stack covers a lot of ground already. But there is a project worth watching if you want to take declarative infrastructure even further: clan.lol.
Clan is a peer-to-peer computer management framework built on top of NixOS. It aims to make secrets management, service provisioning, and backups as declarative as the rest of your system. The nixos-anywhere quickstart guide actually lists Clan as one of its recommended next steps after installation.
We have not explored it deeply yet, but it looks like a natural continuation of the ideas in this post. Maybe we will write about it in the future.