Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Navi is the most complete NixOS deployment tool in existence. Where every other tool solves one slice of the problem, Navi solves all of it. Cloud provisioning, bare-metal installation, disk decryption, secret delivery, DNS, and configuration switching are a single declarative workflow evaluated from one flake. It is the only tool that takes a fleet from nothing to fully running with a single command, and it does exactly that for real, multi-tenant production infrastructure spanning cloud and on-premise hardware today.

Navi began as a fork of Colmena and goes vastly further, unbound by compatibility with its ancestor. Colmena, NixOps, deploy-rs, and morph all begin only once a machine already exists and is reachable. They push a configuration and stop. Navi begins before the machine exists. It creates the machine, installs NixOS onto it, unlocks its encrypted disks, manages its DNS, and delivers its secrets, all from the same Nix evaluation. One source of truth, one command, one tool, instead of a brittle pile of Terraform, nixos-anywhere, a secrets mechanism, a DNS tool, and a deployer lashed together by hand.

Navi is built by longtime Nix maintainers with industrial experience from some of the largest Nix deployments in the world, including a former maintainer of morph. It distills the needs they have run into across their careers into one tool, formalizing what years of operating real fleets taught them about what a deployment tool actually has to do. It is engineered for fleets that are too large, too heterogeneous, and too important to manage by hand. A first-class terminal interface and a persistent daemon let it drive a large fleet concurrently without races, while the same commands scale all the way down to a single workstation. If you run NixOS seriously, Navi is the tool you graduate to.

What it looks like

Everything starts from a Hive: a Nix description of your fleet, living under the navi output of a flake. You write it once, declaring each node's NixOS configuration and how Navi reaches it; The Hive walks through creating one. From then on, because the whole lifecycle is evaluated from that same Hive, the command that acts on one node acts on a whole class of them with the same shape, on cloud or bare metal. Bringing a group of declared machines from nothing to their target configuration is one command:

navi provision --on <selector>

Selecting more nodes widens the operation without changing it, which is what makes a thirty-node fleet no harder to run than a single host. For a concrete picture of what that looks like in practice, see A production deployment.

Descended from Colmena

Navi inherits the Hive model from Colmena, so the configuration format will feel familiar if you have used it. But Navi is not Colmena-compatible and does not aim to be: it has diverged from the start, and it evolves in whatever direction the full lifecycle demands rather than staying within its ancestor's boundaries. What it adds is the rest of the lifecycle: provisioning, installation, disk unlock, DNS, and secret delivery, all from the same evaluation. Two capabilities make this practical at scale — the navi tui terminal interface for managing large fleets interactively, and a background daemon that owns connections and locks so operations across many nodes run concurrently without colliding.

How this manual is organised

Getting started takes you from an empty directory to a deployed node. The topic sections that follow each cover one capability end to end — deploying, providers, provisioning, disk encryption, secrets, and day-to-day operations — bundling the use case, the Hive configuration, and the commands together. The reference documents a complete production fleet, every command, and every configuration option; its command-line section is generated from the binary, so it cannot drift.

Status

Navi is in early development. Commands, configuration formats, and this manual can change between releases without notice. Do not use it with production credentials or on multi-user systems yet, because credential handling is not hardened.

How to read the command examples

Commands are shown without a shell prompt:

navi apply --on web-01

Where a command needs a Hive that an earlier chapter built, the chapter says so at the top.

Installation

Navi needs a working Nix installation with flakes enabled. It does not need to be installed on the machines you deploy to. Those only need an SSH server and a Nix daemon.

Run without installing

You can run Navi straight from the flake:

nix run github:cafkafk/navi -- --help

This is the quickest way to try a command without changing your environment.

Add it to a flake

To pin Navi in a project, add it as an input and use the package it exposes:

{
  inputs.navi.url = "github:cafkafk/navi";

  outputs = { self, nixpkgs, navi, ... }: {
    # navi.packages.<system>.navi
  };
}

Development shell

If you are working on Navi itself, the repository ships a development shell with the Rust toolchain, Nix tooling, and mdBook for this manual:

nix develop

From there, cargo build produces the binary and mdbook serve docs serves this manual locally.

Enabling flakes

If Nix reports that flakes are an experimental feature, add this to your Nix configuration:

experimental-features = nix-command flakes

The rest of the tutorial assumes flakes are enabled.

The Hive

A Hive is the configuration that describes your fleet. It is a Nix attribute set where most attributes are nodes, and a few reserved attributes configure Navi itself. Navi presupposes flakes, so the Hive lives under the navi output of a flake.

Create a flake.nix:

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";

  outputs = { nixpkgs, ... }: {
    navi = {
      meta.nixpkgs = import nixpkgs { system = "x86_64-linux"; };

      defaults = { pkgs, ... }: {
        environment.systemPackages = [ pkgs.vim pkgs.curl ];
      };

      web-01 = { name, nodes, pkgs, ... }: {
        deployment.targetHost = "web-01.example.com";

        services.nginx.enable = true;
        system.stateVersion = "24.11";
      };
    };
  };
}

Navi searches upward from the working directory for a flake.nix unless you pass --config. Because nixpkgs comes from a flake input rather than the channel, evaluation stays pure.

Reserved attributes

Two attribute names are reserved and are not treated as nodes.

meta configures the Hive as a whole. The most important field is nixpkgs, which selects the package set every node is built against. Under flakes it must be set from an input.

defaults is a NixOS module merged into every node. Put configuration that all machines share here, such as common packages, users, or SSH settings.

Nodes

Every other attribute in the navi output is a node, and its value is a NixOS module. The node name is the attribute name. Inside a node, the deployment options control how Navi connects and applies the configuration. The rest is ordinary NixOS.

The most common deployment option is deployment.targetHost, which sets the address Navi connects to. If you omit it, Navi uses the node name as the host.

Sharing modules

Because the Hive is a flake output, it composes with the rest of your Nix configuration. Pin nixpkgs through the input, import shared modules into defaults or individual nodes, and reuse the same flake to expose packages or NixOS configurations elsewhere.

The next chapter deploys this Hive.

Your first deploy

This chapter deploys the Hive from the previous chapter. It assumes you can reach web-01 over SSH and that your user can escalate to root there.

Build before you deploy

Start by building the configuration without touching the target. This catches evaluation and build errors early:

navi apply build --on web-01

Navi evaluates the Hive, then builds the system closure for the selected node. Nothing is copied or activated yet.

Push and activate

When the build succeeds, apply the configuration:

navi apply --on web-01

With no goal given, apply runs the full sequence. It builds the closure, copies it to the target, and activates it as the new system generation. If activation fails, the previous generation stays active, so a bad deploy does not leave the machine in a broken state.

Selecting nodes

The --on flag selects which nodes to act on. It accepts node names, tags, and group expressions, so a single command can target one machine or a whole class of them. With no --on, Navi acts on every node in the Hive.

navi apply --on web-01,web-02
navi apply --on @web

Deployment goals

apply takes an optional goal that stops the sequence early:

  • build evaluates and builds only.
  • push also copies the closure to the target.
  • switch activates immediately and makes the change persist across reboots.
  • boot makes the change take effect on the next reboot only.

Running apply with no goal is equivalent to switch.

You now have a deployed node. The guides cover what happens behind these commands and the features built on top of them.

Applying configurations

navi apply is the core deployment command. It moves a set of nodes from their current state to the configuration described in the Hive.

navi apply --on web-01

With no goal given, apply runs the full sequence and is equivalent to switch. If activation fails, the previous generation stays active, so a bad deploy does not leave the machine broken.

The apply sequence

A full apply runs four stages for each node. It evaluates the node from the Hive, builds the resulting system closure, copies that closure to the target, and activates it. Each stage depends on the one before it, and a failure stops that node without affecting the others.

Deployment goals

apply takes an optional goal, given as a positional argument, that stops the sequence early:

  • build evaluates and builds only.
  • push also copies the closure to the target.
  • switch activates immediately and makes the change persist across reboots.
  • boot makes the change take effect on the next reboot only.

Running apply with no goal is equivalent to switch.

navi apply build --on web-01      # evaluate and build, deploy nothing
navi apply switch --on web-01 -v

The build goal is covered on its own in Builds; use it in continuous integration to verify that every node still evaluates and builds.

Selecting nodes

The --on flag chooses which nodes to act on. It accepts names, globs, and tags, so one command targets a single machine or a whole class of them. With no --on, Navi acts on every node in the Hive. Selection is shared across every subcommand and is documented in Selectors and tags.

navi apply --on web-01,web-02 switch -v
navi apply --on @web switch -v

Verbosity

-v makes a run verbose. It turns off the progress spinner and prints every line of output, which is what you want when a deploy is misbehaving.

Rebooting

Pass --reboot to reboot each node after activation and wait for it to come back before reporting success. Paired with the boot goal, this is how you confirm a configuration survives a real boot, not just a live activation.

navi apply boot --reboot --on web-01
navi apply switch --reboot --on web-01 -v

Parallelism

Navi applies to multiple nodes at once. The daemon manages the connection pool and task queue, so a large fleet deploys concurrently rather than one node at a time. Concurrency limits keep the local machine and the network from being overwhelmed.

Local deployment

To apply the local machine's own configuration without SSH, use navi apply-local. This is the right command for a machine that deploys itself, such as a workstation or a bootstrap host. It needs --sudo to escalate privileges when not run as root:

navi apply-local --sudo -v
navi apply-local boot --sudo -v

The same goals apply. push is a no-op locally, since there is nothing to copy.

Selectors and tags

The --on flag chooses which nodes a command acts on. It is the same selector across apply, build, install, provision, and ssh, so a slice that means something to you means the same thing to every command. With no --on, Navi acts on every node in the Hive.

What --on accepts

The selector is a comma-separated list. Each entry is a node name, a glob, or a tag prefixed with @:

navi apply --on web-01              # one node
navi apply --on web-01,web-02       # several nodes
navi apply --on 'web-*'             # a glob over names
navi apply --on @web                # a tag
navi apply --on @web,@db            # several tags

Quote globs so the shell does not expand them before Navi sees them.

Tags

Tags are declared per node with deployment.tags, a list of strings. A node can carry as many as you like, and a tag can span tenants, environments, and roles:

web-01 = { ... }: {
  deployment.tags = [ "web" "staging" "edge" ];
};

With those tags in place, --on @web selects every web node, --on @staging selects the staging environment, and --on @edge selects the edge tier, regardless of how the nodes are named.

A naming convention

In a large fleet it pays to make the tags fall out of one convention rather than setting them by hand. A small function that stamps the environment, tenant, and tier into each node's name and tags keeps selection predictable:

mkNode = { env, tenant, tier, n }: {
  name = "${env}-${tenant}-${tier}-${toString n}";
  value = {
    imports = [ ./tiers/${tier}.nix ];
    deployment.tags = [ tier env tenant ];
  };
};

Feed a list of those through builtins.listToAttrs to produce the nodes. Every tag then derives from the convention, so --on @ingress, --on @staging, and --on 'acme-*' each address exactly the slice you expect.

Listing what a selector matches

Several commands take --list to show the selected nodes without acting on them. Use it to check a selector before running a destructive operation:

navi install --on 'web-*' --list
navi provision --list

Builds

Building is the part of a deploy that turns a node's NixOS configuration into a system closure. You can build without deploying, and you can choose where the build runs.

Building without deploying

navi build runs the evaluate-and-build stages and stops. Nothing is copied or activated. It is the right check for continuous integration, where you want to know every node still evaluates and builds without touching any of them:

navi build -v
navi build --on 'web-*' -v

The build goal of apply does the same thing for a selection you are about to deploy:

navi apply build --on web-01

Where the build happens

By default Navi builds on the machine that runs the command and copies the result to the target. You can build on the target instead, which helps when the target has resources the local machine lacks, or when you want to avoid copying a large closure over a slow link:

navi build --on web-01 --build-on-target
navi apply --on web-01 --build-on-target -v

Set this per node with deployment.buildOnTarget when a host should always build its own closure:

web-01 = { ... }: {
  deployment.buildOnTarget = true;
};

The command-line flag overrides the node setting for a single run; --no-build-on-target forces a local build even when the node opts in.

Passing options to Nix

--nix-option forwards an arbitrary option to the underlying Nix commands. It only takes effect when building locally. Use it, for example, to point a build at a specific binary cache:

navi apply-local --sudo -v --nix-option substituters https://cache.example.org

Providers

A provider is where a node's machine comes from. Navi's reason to exist is that it drives the whole lifecycle from one declarative source — creating the machine, installing NixOS onto it, and switching it to its configuration — and the provider is the first link in that chain.

The lifecycle Navi covers is:

  1. Provisioning, creating the machine through Terraform.
  2. Installation, putting NixOS onto it with nixos-anywhere.
  3. Disk decryption, unlocking encrypted volumes in initrd.
  4. Configuration switching, activating the target NixOS configuration.

Without Navi these are separate tools stitched together by hand. With Navi they are one Nix-evaluated workflow, because the whole lifecycle is evaluated from the same Hive. An address that provisioning assigns becomes a fact that installation and switching read, without you copying it between tools.

Declaring a provider on a node

Provider settings live in a node's deployment block. They tell Navi how the machine is created and how to reach it. The two providers documented here are:

  • Google Cloud, for GCP virtual machines, including reaching instances with no public address through an Identity-Aware Proxy tunnel.
  • Bare metal, for physical hosts you install over the network.

A node that has no provider — one that already exists and is reachable over SSH — needs nothing here beyond deployment.targetHost. Navi begins managing it from the apply step, the same point Colmena would.

What it scales to

The same command shape that deploys one node deploys a whole class of them, across both cloud and bare metal. Selecting more nodes widens an operation rather than adding steps to it, which is what makes a thirty-node fleet no harder to run than a single host.

Google Cloud

Navi has native support for Google Cloud Platform. It provisions instances through Terraform, installs NixOS onto them, and reaches them afterwards — over a public address, or through an Identity-Aware Proxy tunnel for instances that have none.

Declaring a GCP node

A node's GCP settings live under deployment.providers.gcp. They name the project and zone the instance lives in, and whether to reach it through IAP:

"staging-acme-app-1" = { ... }: {
  deployment.tags = [ "app" "staging" "acme" ];
  deployment.provisioner = "acme";
  deployment.providers.gcp = {
    project = "<project-id>";
    zone = "europe-west3-a";
    iap = true;
  };
};

deployment.provisioner ties the node to the provisioner that creates its infrastructure; see Provisioners and facts.

Identity-Aware Proxy

iap controls whether Navi tunnels SSH through Google's Identity-Aware Proxy. It defaults to true. With IAP, you can deploy to instances that have no public address: Navi establishes the tunnel and runs the deploy through it, so the same apply, ssh, and exec commands work whether or not the machine is exposed.

navi apply --on @app switch -v
navi ssh staging-acme-app-1

No extra flags are needed at the command line — the tunnel is set up from the node's declared providers.gcp settings.

Authentication

Navi uses your local Google Cloud credentials to provision and to open IAP tunnels. Authenticate with gcloud before running provisioning or connecting, the same way you would for any other GCP tooling.

Bare metal

A bare-metal node is a physical machine you install over the network rather than one a cloud creates for you. Navi installs NixOS onto it with nixos-anywhere and then manages it like any other node.

Installing onto a fresh machine

A fresh machine has no facts to read an address from, so you give Navi the address and the installer's initial password directly. Booting the target into a NixOS installer or any SSH-reachable Linux is enough to begin:

navi provision --on rack-01 --ip 10.0.0.20 --initial-password <installer-password>

--ip skips the interactive address prompt, and --initial-password is passed through to nixos-anywhere for the first SSH connection, before the machine has your keys. Once installation finishes, the host is keyed and subsequent commands connect normally.

Reaching hosts on a LAN

Bare-metal hosts often sit on a local network behind an overlay such as Tailscale. To force SSH through a physical interface rather than the overlay, set deployment.forceHwLink on the node. Navi detects an appropriate physical interface and routes the connection through it:

rack-01 = { ... }: {
  deployment.tags = [ "on-prem" ];
  deployment.forceHwLink = true;
};

The initrd unlock connection has its own equivalent, deployment.unlock.forceHwLink, covered in Remote unlock.

Reinstalling

To wipe and reinstall a node from scratch, pass --reinstall. This destroys and recreates the machine's installation before installing onto it again:

navi install --on rack-01 --reinstall

Inspect the selection first with --list when you want to be sure which hosts a reinstall would touch.

Provisioners and facts

Provisioning is where Navi's lifecycle begins, before any machine exists. It creates the machines, installs NixOS onto them, and hands off to the configuration switch, all from the same Hive. Navi integrates Terranix and Terraform so that infrastructure and NixOS configuration live in one place.

The model

You declare provisioners under meta.provisioners and point a node at one with deployment.provisioner. Navi renders the Terranix expressions to Terraform, runs Terraform to create the resources, and captures the outputs as facts. Those facts — such as a freshly assigned IP address — become available to the rest of the deployment without you copying them between tools.

This closes the gap between provisioning and configuration. A single command can stand up cloud instances, install NixOS onto them, and switch them to their target configuration.

Provisioning commands

navi provision drives the Terraform lifecycle. With no argument it provisions everything; name a provisioner, or select nodes with --on, to scope it. It plans, applies, and manages the Terraform lock and state so that concurrent runs do not corrupt each other.

navi provision                      # everything
navi provision acme                 # one provisioner
navi provision --on 'acme-*'        # a slice of nodes
navi provision --list               # what is available

Common flags shape a run:

navi provision acme --reprovision      # destroy and recreate
navi provision acme --skip-install     # infrastructure only, no OS install
navi provision acme --update-tf-lock   # refresh the Terraform lock

Importing existing infrastructure into Navi's state is navi provision import.

Bootstrapping fresh machines

After provisioning creates a machine, it is a bare host with no NixOS on it. Navi integrates nixos-anywhere to install NixOS over SSH, turning a blank instance into a managed node. The navi install command loads the captured outputs and bootstraps the target:

navi install --on acme-app-1
navi install --on acme-app-1 --reinstall

For bare-metal hosts, the address can come from a flag rather than Terraform outputs; see Bare metal.

Provisioning at scale

Because selection accepts globs and tags, one command stands up a whole class of nodes. When a class is expensive to bring up and should be provisioned one at a time, list it and walk the list so a failure stops at the node it happened on:

navi provision --on 'acme-*'

for p in (navi provision --list)
    navi provision $p
end

Facts

Outputs captured from Terraform are stored as facts and persist between runs. Facts are how provisioning hands information to deployment. They are configured under meta.facts, and you can pre-compute a subset before provisioning with navi facts derive:

navi facts derive

DNS and registrars

Provisioning can register and update DNS records as part of the lifecycle, so a machine that provisioning creates is reachable by name without a separate step in a registrar's console. Registrar credentials are declared under meta.registrants, with one entry per registrar.

Credentials always come from a command rather than being written into the Hive, so no secret is committed. Each supported registrar has its own subsection:

How records get applied

DNS is part of provisioning, so records are created and updated when you run navi provision. Because the registrar credentials and the records are declared in the same Hive as the machines, a single provisioning run can stand up an instance and publish its name together.

Porkbun

A Porkbun account is declared under meta.registrants.porkbun, keyed by an account name. It is given the commands that produce its API credentials:

meta.registrants.porkbun.<account> = {
  apiKeyCommand = "pass show dns/porkbun/api-key";
  secretApiKeyCommand = "pass show dns/porkbun/secret-key";
};

Both apiKeyCommand and secretApiKeyCommand are local commands whose output is the credential, so the secrets stay out of the Hive.

Credentials as Terraform variables

For Porkbun the credentials are injected as Terraform variables by default, so the DNS resources you declare in Terranix can use them without further wiring. The relevant fields are:

  • terraformSecrets controls the injection. It defaults to true; set it to false to keep the credentials out of Terraform's variables.
  • apiKeyVariable and secretKeyVariable set the variable names Terraform sees. They default to porkbun_api_key and porkbun_secret_api_key.

The defaults are usually what you want. Override the variable names only when a module you are using expects different ones:

meta.registrants.porkbun.<account> = {
  apiKeyCommand = "pass show dns/porkbun/api-key";
  secretApiKeyCommand = "pass show dns/porkbun/secret-key";
  apiKeyVariable = "pb_key";
  secretKeyVariable = "pb_secret";
};

Namecheap

A Namecheap account is declared under meta.registrants.namecheap, keyed by an account name. It takes the commands that produce its API key and its API user:

meta.registrants.namecheap.<account> = {
  apiKeyCommand = "pass show dns/namecheap/api-key";
  userCommand = "pass show dns/namecheap/user";
};

Both fields are local commands whose output is the credential, so nothing sensitive is written into the Hive:

  • apiKeyCommand outputs the API key.
  • userCommand outputs the API user the key belongs to.

With the account declared, DNS records for its domains are created and updated as part of navi provision, the same as any other registrar.

Remote unlock

A host with an encrypted root cannot finish booting on its own. It comes up into a minimal initrd, joins the network, and waits for someone to supply the passphrase before the real system can start. Navi drives that unlock remotely, so an encrypted machine rejoins the fleet without a console or a person in the room.

This is the disk-decryption step of the lifecycle: the node is provisioned and installed, but each boot pauses until its volumes are unlocked.

The NixOS side

Encrypted hosts run an SSH server in initrd so Navi can reach them before the disk is open. On the NixOS side this is the usual boot.initrd.network and boot.initrd.network.ssh configuration that brings up networking and an early-boot SSH daemon with its own host key and authorized keys.

Declaring unlock on a node

Navi's side is deployment.unlock. It tells Navi how to connect to the initrd and what to run there. The defaults assume a ZFS root on the standard initrd SSH port:

rack-01 = { ... }: {
  deployment.unlock = {
    enable = true;
    passwordCommand = "pass show hosts/rack-01/zfs";
    remoteCommand = "zfs load-key -a && killall zfs";
  };
};

The important fields are:

  • enable turns on unlock support for the node.
  • passwordCommand is a local command whose output is the passphrase. Keeping it a command means the passphrase is never written into the Hive.
  • remoteCommand is what runs in the initrd to load the keys. The default imports the pool and loads ZFS keys; override it for other setups.
  • port, host, and user override the initrd SSH endpoint when it differs from the running system's. port defaults to 2222.
  • forceHwLink routes the unlock connection through a physical interface, bypassing an overlay network such as Tailscale, for hosts reached on a LAN.

Unlocking a host

With unlock declared, navi disk-unlock connects to the initrd and runs the unlock for you:

navi disk-unlock rack-01

The passphrase is fetched by the node's passwordCommand, and the remoteCommand loads the keys. Once they are loaded, initrd proceeds and the host finishes booting.

Recovering a host after reboot

When a host reboots and stalls waiting for its passphrase, the same command brings it back. The command-line flags override the declared settings for a single run, which is useful during recovery when the address or credentials differ from the norm:

navi disk-unlock rack-01 \
  --password-cmd "pass show hosts/rack-01/zfs" \
  --remote-cmd "zfs load-key -a && killall zfs" \
  --address 10.0.0.10 \
  --user root

The same shape drives lower-level recovery — inspect the pool, mount a dataset, or force a reboot — by changing only the remote command:

navi disk-unlock rack-01 --address 10.0.0.10 --user root \
  --remote-cmd "zpool status; echo '-----'; mount"

navi disk-unlock rack-01 --address 10.0.0.10 --user root \
  --remote-cmd "reboot -f"

Keys

Keys are secret files Navi delivers to a node out of band, rather than baking them into the world-readable Nix store. They are declared under deployment.keys and uploaded as part of a deploy, or on their own with navi upload-keys.

Declaring a key

Each entry under deployment.keys is a file to place on the host. Its contents come from inline text, a local file, or a command — and a command keeps the secret out of the Hive entirely:

web-01 = { ... }: {
  deployment.keys."tls.key" = {
    keyCommand = [ "pass" "show" "hosts/web-01/tls-key" ];
    destDir = "/run/keys";
    user = "nginx";
    group = "nginx";
    permissions = "0400";
  };
};

Exactly one of text, keyFile, or keyCommand must be set. The remaining fields control where the key lands and who may read it:

  • destDir is the directory on the host; it defaults to /run/keys.
  • name is the file name, defaulting to the attribute name.
  • user, group, and permissions set ownership and mode, defaulting to root:root and 0600.

When keys are uploaded

uploadAt controls timing relative to activation:

  • pre-activation (the default) uploads the key before the new profile is activated, so a service that needs it at startup finds it in place.
  • post-activation uploads after a successful activation.

During an apply, keys are uploaded automatically according to this setting. Pass --no-keys to skip key upload for a run.

Uploading keys on their own

navi upload-keys uploads every declared key without doing a full deploy. This is the way to rotate a secret, or to place keys on a host before its first activation:

navi upload-keys --on web-01

When invoked this way, all keys are uploaded together regardless of their individual uploadAt setting.

SSH and exec

Once a fleet is running, you still need to get onto a single machine to look at it, or run one command across a group. Navi exposes both using the same connection details it deploys with, including IAP tunnels for cloud nodes with no public address.

SSH into a host

navi ssh opens an interactive session on a node:

navi ssh web-01

Append a command after -- to run it and return, rather than opening a shell. This is the quick way to poke at one node during an incident:

navi ssh web-01 -- systemctl restart nginx

Connect as root with -R, and select the host with --on when you prefer the same selector syntax the other commands use:

navi ssh -R --on web-01

Running a command across hosts

navi exec runs a single command on every selected node without opening a session. It is the right tool when you want the same thing done on a group and want to see each node's output:

navi exec uptime
navi exec --on @web -- systemctl is-active nginx

Because exec takes the standard selector, it scales from one node to a whole tier without changing shape. For interactive work on a single machine, reach for ssh; for a fan-out of one command, reach for exec.

Serial console

When a node is unreachable over SSH — wedged mid-boot, with a broken network configuration, or otherwise off the network — the serial console is the way in. navi serial connects to a node's console so you can watch it boot or log in when nothing else works.

navi serial rack-01

This is the fallback for situations the other commands cannot reach. A node that will not come up far enough to accept an SSH connection, or whose last deploy broke its networking, can still be driven from its serial console. For an encrypted host that is simply waiting on its passphrase, prefer Remote unlock — the serial console is for when the machine needs hands-on attention beyond that.

The terminal interface

navi tui opens a terminal interface for managing a fleet interactively. It is built for the case where you have more nodes than you can track from a stream of log lines.

Start it with:

navi tui

Node view

Nodes are shown in a hierarchy that you can organise by category, environment, or hostgroup. This lets you collapse a large fleet down to the part you care about and act on a whole group at once.

Monitoring

The interface shows live state for the fleet. You can watch logs as they arrive, see RAM usage, and follow active tasks across every node, rather than reading a single combined output stream.

Acting on nodes

Selection in the node view drives actions. You can pick specific nodes or a group and then deploy them, run garbage collection, or apply locally, without leaving the interface. This is the same set of operations the command line exposes, driven by selection instead of flags.

Inspection

For any node you can view its metadata: its address, its tags, and the git revision it is running against. This makes it quick to see which machines are behind, without running a separate query for each one.

Logs

The interface aggregates logs from local and remote operations and lets you filter them. When a deploy touches many nodes, this is how you find the one that failed.

The daemon

Navi runs a background daemon that owns connections, task queues, and deployment state. Commands you run on the command line are clients that talk to this daemon rather than doing the work in their own process.

Why a daemon

Splitting the work from the client process buys three things. It holds a single connection pool, so repeated commands against the same fleet reuse connections. It serialises operations that must not overlap, which prevents two applies from racing on the same node. And it lets long operations continue after the client that started them exits, so a deploy survives a closed terminal.

Managing the daemon

The daemon is controlled through the navi daemon subcommand. Use it to start the daemon, check its status, and stop it.

A client starts the daemon automatically when one is not already running, so for everyday use you rarely call these commands directly. Manage the daemon explicitly when you want it to run as a system service, or when you are debugging.

State and locks

The daemon is the single owner of deployment locks. When an apply holds a lock on a node, other operations against that node wait rather than colliding. This is what makes it safe to run overlapping commands against a shared fleet.

Because the daemon holds this state, stopping it cancels in-flight operations. Stop it only when no deploy is running, or accept that running operations are aborted.

A production deployment

This chapter sketches a real fleet managed with Navi. It is here to show what the workflow looks like at scale, rather than on the single node the tutorial builds. The names below are placeholders; the shape is what matters.

The fleet is heterogeneous and lives in one flake:

  • Around thirty GCP virtual machines, grouped across several tenants and environments. Many have no public address and are reached through an Identity-Aware Proxy tunnel.
  • On-prem hardware running on encrypted ZFS, unlocked remotely as part of a deploy.

Within each tenant the machines fall into tiers — an edge tier that terminates DNS, a gateway, ingress nodes, the application nodes behind them, and an observability tier. The same tier shape repeats across tenants and across the staging and prod environments.

From that single description, operations that would otherwise be multi-step runbooks reduce to one command each.

The NixOS configuration it deploys

Every command below operates on ordinary NixOS configuration. The fleet lives in a flake, under the navi output. nixpkgs comes from a flake input rather than the channel, so evaluation stays pure. Each node is a NixOS module — the same one you would write without Navi — plus a small deployment block that tells Navi how to reach it and which slices it belongs to. The tags are what the @ selectors match:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
    navi.url = "github:cafkafk/navi";
  };

  outputs = { nixpkgs, ... }: {
    navi = {
      meta.nixpkgs = import nixpkgs { system = "x86_64-linux"; };

      # Merged into every node.
      defaults = { pkgs, ... }: {
        services.openssh.enable = true;
        system.stateVersion = "24.11";
      };

      "staging-<tenant>-ingress-1" = { ... }: {
        deployment.tags = [ "ingress" "staging" "<tenant>" ];

        services.nginx.enable = true;
      };
    };
  };
}

The remaining snippets are entries inside that navi output. A cloud node adds its provider, so Navi can reach it through the proxy tunnel when it has no public address, and names the provisioner that creates it:

"staging-<tenant>-app-1" = { ... }: {
  deployment.tags = [ "app" "staging" "<tenant>" ];
  deployment.provisioner = "<tenant>";
  deployment.providers.gcp = {
    project = "<project-id>";
    zone = "europe-west3-a";
    iap = true;              # reach it over Identity-Aware Proxy
  };
  deployment.buildOnTarget = true;
};

An on-prem node declares how its encrypted disk is unlocked, which is what navi disk-unlock drives. The password command runs locally; the remote command runs in the node's initrd:

"rack-01" = { ... }: {
  deployment.tags = [ "on-prem" ];
  deployment.unlock = {
    enable = true;
    passwordCommand = "pass show hosts/rack-01/zfs";
    remoteCommand = "zfs load-key -a && killall zfs";
  };
};

The tier shape repeats across tenants and environments, so in practice the nodes are generated rather than written out one by one. A small function stamps the environment, tenant, and tier into each node's name and tags, and merges in that tier's shared module:

mkNode = { env, tenant, tier, n }: {
  name = "${env}-${tenant}-${tier}-${toString n}";
  value = {
    imports = [ ./tiers/${tier}.nix ];
    deployment.tags = [ tier env tenant ];
    deployment.provisioner = tenant;
  };
};

Feeding a list of those through builtins.listToAttrs is what produces the thirty-odd nodes. Because every tag falls out of one naming convention, the selectors in the sections below — @ingress, @staging, <tenant>-* — each address exactly the slice you expect.

Selecting slices of the fleet

Nodes follow a naming convention and carry tags, so --on can slice the fleet any way the work demands — by tenant, by environment, by tier, or by a single host. A glob widens the selection; a tag prefixed with @ matches a role that spans tenants:

navi apply --on '<tenant>-*'        # one tenant, every tier
navi apply --on @staging            # one environment, every tenant
navi apply --on @ingress            # one tier, everywhere it appears
navi apply --on '<tenant>-app-1'    # a single node

The selector is the same across provision, install, apply, build, and ssh, so a slice that means something to you means the same thing to every command.

Standing up a tenant

Creating every machine for a tenant, installing NixOS onto them, and switching them to their configuration is one command. The machines do not exist when it starts and are serving their configuration when it finishes:

navi provision --on '<tenant>-*'

This replaces a sequence of Terraform, nixos-anywhere, and Colmena runs. When a tenant needs its infrastructure rebuilt but not reinstalled, scope it down:

navi provision --on '<tenant>-*' --skip-install --reprovision

Some classes of node are expensive to bring up and are provisioned one at a time rather than all at once. Listing them and walking the list keeps each one isolated, so a failure stops at the node it happened on:

for p in (navi provision --list)
    navi provision $p
end

Rolling out a change in order

A change rarely lands on the whole fleet at once. The edge and gateway tiers go first, then the nodes behind them, so traffic keeps flowing through a node that is already on the new configuration. Selecting more nodes widens each step without changing its shape:

navi apply --on @gateway,@ingress switch -v
navi apply --on @app switch -v

Nodes whose change touches the boot path reboot as part of the rollout, and Navi waits for each to come back before reporting success:

navi apply --on @ingress switch -v --reboot

If activation fails on a node, the previous generation stays active, so a bad rollout does not take the tier down with it.

Reaching a single node

When one node misbehaves, open a session on it with the same connection details Navi deploys with, or run a single command and return. This works through the proxy tunnel for machines with no public address:

navi ssh <tenant>-edge-1
navi ssh <tenant>-edge-1 -- systemctl restart bind

Bringing up encrypted hardware

The on-prem machines boot into an encrypted ZFS volume and stall in initrd until it is unlocked. Unlocking one remotely lets it finish booting and rejoin the fleet:

navi disk-unlock <node>

Watching the fleet

For day-to-day operation across this many nodes, the terminal interface shows the fleet by tenant, environment, or tier, with live logs and task state:

navi tui

The point

None of these commands is doing something a pile of separate tools could not do. What changes is that they come from one description and one tool. The fleet has a single source of truth, and scaling it up is a matter of selecting more nodes, not adding more steps.

Command line

This document contains the help content for the navi command-line program.

Command Overview:

NixOS deployment tool

Navi helps you deploy to multiple hosts running NixOS. For more details, read the manual at https://navi.cli.rs/0.0.

Note: You are using a pre-release version of Navi, so the supported options may be different from what's in the manual.

Usage: navi [OPTIONS] <COMMAND>

Subcommands:
  • apply — Apply configurations on remote machines
  • apply-local — Apply configurations on the local machine
  • build — Build configurations but not push to remote hosts
  • eval — Evaluate an expression using the complete configuration
  • upload-keys — Upload keys to remote hosts
  • exec — Run a command on remote machines
  • list — List available hosts and their configuration
  • repl — Start an interactive REPL with the complete configuration
  • nix-info — Show information about the current Nix installation
  • tui — Start an interactive TUI dashboard
  • serial — Connect to node serial console
  • ssh — SSH into a host
  • disk-unlock — Unlock a disk on a remote host
  • facts — Manage persistent facts
  • provision — Provision infrastructure for nodes
  • install — Install NixOS on target nodes
  • daemon — Start the Navi Daemon
Options:
  • -f, --config <CONFIG> — If this argument is not specified, Navi will search upwards from the current working directory for a file named "flake.nix" or "hive.nix". This behavior is disabled if --config/-f is given explicitly.

    For a sample configuration, check the manual at https://navi.cli.rs/0.0.

  • --show-trace — Show debug information for Nix commands

    Passes --show-trace to Nix commands

  • --impure — Allow impure expressions

    Passes --impure to Nix commands

  • --nix-option <NAME> — Passes an arbitrary option to Nix commands

    This only works when building locally.

  • --color <WHEN> — When to colorize the output

    By default, Navi enables colorized output when the terminal supports it.

    It's also possible to specify the preference using environment variables. See https://bixense.com/clicolors.

    Default value: auto

    Possible values:

    • auto: Detect automatically
    • always: Always display colors
    • never: Never display colors

Apply configurations on remote machines

Usage: navi apply [OPTIONS] [GOAL]

Arguments:
  • <GOAL> — Deployment goal

    Same as the targets for switch-to-configuration, with the following extra pseudo-goals:

    • build: Only build the system profiles - push: Only copy the closures to remote nodes - keys: Only upload the keys to the remote nodes

    switch is the default goal unless --reboot is passed, in which case boot is the default.

    Default value: switch

    Possible values:

    • build: Build the configurations only
    • push: Push the closures only
    • switch: Make the configuration the boot default and activate now
    • boot: Make the configuration the boot default
    • test: Activate the configuration, but don't make it the boot default
    • dry-activate: Show what would be done if this configuration were activated
    • upload-keys: Only upload keys
Options:
  • --eval-node-limit <LIMIT> — Evaluation node limit

    Limits the maximum number of hosts to be evaluated at once. The evaluation process is RAM-intensive. The default behavior is to limit the maximum number of hosts evaluated at the same time based on naive heuristics.

    Set to 0 to disable the limit.

    Default value: auto

  • -p, --parallel <LIMIT> — Deploy parallelism limit

    Limits the maximum number of hosts to be deployed in parallel.

    Set to 0 to disable parallelism limit.

    Default value: 10

  • --keep-result — Create GC roots for built profiles.

    The built system profiles will be added as GC roots so that they will not be removed by the garbage collector. The links will be created under .gcroots in the directory the Hive configuration is located.

  • -v, --verbose — Be verbose

    Deactivates the progress spinner and prints every line of output.

  • --no-keys — Do not upload keys

    By default, Navi will upload secret keys set in deployment.keys before deploying the new profile on a node. To upload keys without building or deploying the rest of the configuration, use navi upload-keys.

  • --reboot — Reboot nodes after activation

    Reboots nodes after activation and waits for them to come back up.

  • --no-substitute — Do not use substitutes

    Disables the use of substituters when copying closures to the remote host.

  • --no-gzip — Do not use gzip

    Disables the use of gzip when copying closures to the remote host.

  • --build-on-target — Build the system profiles on the target nodes

    If enabled, the system profiles will be built on the target nodes themselves, not on the host running Navi. This overrides per-node preferences set in deployment.buildOnTarget. To temporarily disable remote build on all nodes, use --no-build-on-target.

  • --force-replace-unknown-profiles — Ignore all targeted nodes deployment.replaceUnknownProfiles setting

    If deployment.replaceUnknownProfiles is set for a target, using this switch will treat deployment.replaceUnknownProfiles as though it was set to true and perform unknown profile replacement.

  • --install-bootloader — Install bootloader

    If enabled, the bootloader will be installed during activation. This is equivalent to NIXOS_INSTALL_BOOTLOADER=1.

  • --evaluator <EVALUATOR> — The evaluator to use (experimental)

    If set to chunked (default), evaluation of nodes will happen in batches. If set to streaming, the experimental streaming evaluator (nix-eval-jobs) will be used and nodes will be evaluated in parallel.

    This is an experimental feature.

    Default value: chunked

    Possible values: chunked, streaming

  • --on <NODES> — Node selector

    Select a list of nodes to deploy to. The list is comma-separated and globs are supported. To match tags, prepend the filter by @. Valid examples:

    • host1,host2,host3 - edge-* - edge-,core- - @a-tag,@tags-can-have-*

Apply configurations on the local machine

Usage: navi apply-local [OPTIONS] [GOAL]

Arguments:
  • <GOAL> — Deployment goal

    Same as the targets for switch-to-configuration. "push" is noop in apply-local.

    Default value: switch

    Possible values:

    • build: Build the configurations only
    • push: Push the closures only
    • switch: Make the configuration the boot default and activate now
    • boot: Make the configuration the boot default
    • test: Activate the configuration, but don't make it the boot default
    • dry-activate: Show what would be done if this configuration were activated
    • upload-keys: Only upload keys
Options:
  • --sudo — Attempt to escalate privileges if not run as root

  • -v, --verbose — Be verbose

    Deactivates the progress spinner and prints every line of output.

  • --no-keys — Do not deploy keys

    Do not deploy secret keys set in deployment.keys. By default, Navi will deploy keys set in deployment.keys before activating the profile on this host.

  • --install-bootloader — Install bootloader

    If enabled, the bootloader will be installed during activation. This is equivalent to NIXOS_INSTALL_BOOTLOADER=1.

  • --node <NODE> — Override the node name to use

Build configurations but not push to remote hosts

This subcommand behaves as if you invoked apply with the build goal.

Usage: navi build [OPTIONS]

Options:
  • --eval-node-limit <LIMIT> — Evaluation node limit

    Limits the maximum number of hosts to be evaluated at once. The evaluation process is RAM-intensive. The default behavior is to limit the maximum number of hosts evaluated at the same time based on naive heuristics.

    Set to 0 to disable the limit.

    Default value: auto

  • -p, --parallel <LIMIT> — Deploy parallelism limit

    Limits the maximum number of hosts to be deployed in parallel.

    Set to 0 to disable parallelism limit.

    Default value: 10

  • --keep-result — Create GC roots for built profiles.

    The built system profiles will be added as GC roots so that they will not be removed by the garbage collector. The links will be created under .gcroots in the directory the Hive configuration is located.

  • -v, --verbose — Be verbose

    Deactivates the progress spinner and prints every line of output.

  • --no-keys — Do not upload keys

    By default, Navi will upload secret keys set in deployment.keys before deploying the new profile on a node. To upload keys without building or deploying the rest of the configuration, use navi upload-keys.

  • --reboot — Reboot nodes after activation

    Reboots nodes after activation and waits for them to come back up.

  • --no-substitute — Do not use substitutes

    Disables the use of substituters when copying closures to the remote host.

  • --no-gzip — Do not use gzip

    Disables the use of gzip when copying closures to the remote host.

  • --build-on-target — Build the system profiles on the target nodes

    If enabled, the system profiles will be built on the target nodes themselves, not on the host running Navi. This overrides per-node preferences set in deployment.buildOnTarget. To temporarily disable remote build on all nodes, use --no-build-on-target.

  • --force-replace-unknown-profiles — Ignore all targeted nodes deployment.replaceUnknownProfiles setting

    If deployment.replaceUnknownProfiles is set for a target, using this switch will treat deployment.replaceUnknownProfiles as though it was set to true and perform unknown profile replacement.

  • --install-bootloader — Install bootloader

    If enabled, the bootloader will be installed during activation. This is equivalent to NIXOS_INSTALL_BOOTLOADER=1.

  • --evaluator <EVALUATOR> — The evaluator to use (experimental)

    If set to chunked (default), evaluation of nodes will happen in batches. If set to streaming, the experimental streaming evaluator (nix-eval-jobs) will be used and nodes will be evaluated in parallel.

    This is an experimental feature.

    Default value: chunked

    Possible values: chunked, streaming

  • --on <NODES> — Node selector

    Select a list of nodes to deploy to. The list is comma-separated and globs are supported. To match tags, prepend the filter by @. Valid examples:

    • host1,host2,host3 - edge-* - edge-,core- - @a-tag,@tags-can-have-*

Evaluate an expression using the complete configuration

Your expression should take an attribute set with keys pkgs, lib and nodes (like a NixOS module) and return a JSON-serializable value. For example, to retrieve the configuration of one node, you may write something like:

{ nodes, ... }: nodes.node-a.config.networking.hostName

Usage: navi eval [OPTIONS] [FILE]

Arguments:
  • <FILE> — The .nix file containing the expression
Options:
  • -E <EXPRESSION> — The Nix expression
  • --instantiate — Actually instantiate the expression

Upload keys to remote hosts

This subcommand behaves as if you invoked apply with the pseudo keys goal.

Usage: navi upload-keys [OPTIONS]

Options:
  • --eval-node-limit <LIMIT> — Evaluation node limit

    Limits the maximum number of hosts to be evaluated at once. The evaluation process is RAM-intensive. The default behavior is to limit the maximum number of hosts evaluated at the same time based on naive heuristics.

    Set to 0 to disable the limit.

    Default value: auto

  • -p, --parallel <LIMIT> — Deploy parallelism limit

    Limits the maximum number of hosts to be deployed in parallel.

    Set to 0 to disable parallelism limit.

    Default value: 10

  • --keep-result — Create GC roots for built profiles.

    The built system profiles will be added as GC roots so that they will not be removed by the garbage collector. The links will be created under .gcroots in the directory the Hive configuration is located.

  • -v, --verbose — Be verbose

    Deactivates the progress spinner and prints every line of output.

  • --no-keys — Do not upload keys

    By default, Navi will upload secret keys set in deployment.keys before deploying the new profile on a node. To upload keys without building or deploying the rest of the configuration, use navi upload-keys.

  • --reboot — Reboot nodes after activation

    Reboots nodes after activation and waits for them to come back up.

  • --no-substitute — Do not use substitutes

    Disables the use of substituters when copying closures to the remote host.

  • --no-gzip — Do not use gzip

    Disables the use of gzip when copying closures to the remote host.

  • --build-on-target — Build the system profiles on the target nodes

    If enabled, the system profiles will be built on the target nodes themselves, not on the host running Navi. This overrides per-node preferences set in deployment.buildOnTarget. To temporarily disable remote build on all nodes, use --no-build-on-target.

  • --force-replace-unknown-profiles — Ignore all targeted nodes deployment.replaceUnknownProfiles setting

    If deployment.replaceUnknownProfiles is set for a target, using this switch will treat deployment.replaceUnknownProfiles as though it was set to true and perform unknown profile replacement.

  • --install-bootloader — Install bootloader

    If enabled, the bootloader will be installed during activation. This is equivalent to NIXOS_INSTALL_BOOTLOADER=1.

  • --evaluator <EVALUATOR> — The evaluator to use (experimental)

    If set to chunked (default), evaluation of nodes will happen in batches. If set to streaming, the experimental streaming evaluator (nix-eval-jobs) will be used and nodes will be evaluated in parallel.

    This is an experimental feature.

    Default value: chunked

    Possible values: chunked, streaming

  • --on <NODES> — Node selector

    Select a list of nodes to deploy to. The list is comma-separated and globs are supported. To match tags, prepend the filter by @. Valid examples:

    • host1,host2,host3 - edge-* - edge-,core- - @a-tag,@tags-can-have-*

Run a command on remote machines

Usage: navi exec [OPTIONS] <COMMAND>...

Arguments:
  • <COMMAND> — Command to run

    It's recommended to use -- to separate Navi options from the command to run. For example:

    navi exec --on @routers -- tcpdump -vni any ip[9] == 89

Options:
  • -p, --parallel <LIMIT> — Deploy parallelism limit

    Limits the maximum number of hosts to run the command in parallel.

    In navi exec, the parallelism limit is disabled (0) by default.

    Default value: 0

  • -v, --verbose — Be verbose

    Deactivates the progress spinner and prints every line of output.

  • --on <NODES> — Node selector

    Select a list of nodes to deploy to. The list is comma-separated and globs are supported. To match tags, prepend the filter by @. Valid examples:

    • host1,host2,host3 - edge-* - edge-,core- - @a-tag,@tags-can-have-*

List available hosts and their configuration

Usage: navi list [OPTIONS]

Options:
  • -j, --json — Output in JSON format

  • --group-by <KEY> — Group hosts into sections by the given key

    Possible values:

    • tag: One section per tag (hosts with several tags appear in each)
    • provisioner: One section per provisioner
    • link: One section per connection link (SSH, GCP, ...)

Start an interactive REPL with the complete configuration

In the REPL, you can inspect the configuration interactively with tab completion. The node configurations are accessible under the nodes attribute set.

Usage: navi repl

Show information about the current Nix installation

Usage: navi nix-info

Start an interactive TUI dashboard

Usage: navi tui [OPTIONS]

Options:
  • -p, --parallel <PARALLEL> — Limit the maximum number of hosts to be deployed in parallel

    Default value: 10

  • --no-daemon — Disable daemon and force local execution

    Default value: false

Connect to node serial console

Usage: navi serial <NODE>

Arguments:
  • <NODE> — The node to connect to

SSH into a host

Usage: navi ssh [OPTIONS] [HOST] [-- <COMMAND>...]

Arguments:
  • <HOST> — Host to connect to (required unless -R is used)
  • <COMMAND> — Command and arguments to pass to SSH
Options:
  • -R, --remove-keys — Remove host keys from known_hosts

  • --on <NODES> — Node selector

    Select a list of nodes to deploy to. The list is comma-separated and globs are supported. To match tags, prepend the filter by @. Valid examples:

    • host1,host2,host3 - edge-* - edge-,core- - @a-tag,@tags-can-have-*

Unlock a disk on a remote host

Usage: navi disk-unlock <HOST>

Arguments:
  • <HOST> — Host to unlock

Manage persistent facts

Usage: navi facts <COMMAND>

Subcommands:
  • derive — Derive facts from the Nix configuration (Pre-computation)

Derive facts from the Nix configuration (Pre-computation)

Usage: navi facts derive [FILTERS]...

Arguments:
  • <FILTERS> — Filter facts to generate (glob patterns)

Provision infrastructure for nodes

Usage: navi provision [OPTIONS] [PROVISIONER]

Arguments:
  • <PROVISIONER> — Explicitly select a provisioner to run
Options:
  • --list — List available provisioners

  • --reprovision — Destroy and recreate the infrastructure

  • --unlock — Unlock the disk after deployment

  • --update-tf-lock <UPDATE_TF_LOCK> — Update the local Terraform lock file from the sandbox state

  • --skip-install — Skip the OS installation step (nixos-anywhere), only provision infrastructure and update facts

  • --ip <IP> — IP address for bare-metal provisioning (skips interactive prompt)

  • --initial-password <INITIAL_PASSWORD> — Password for initial SSH connection to the installer (e.g. for bare-metal hosts that haven't been keyed yet). Passed to nixos-anywhere via --env-password

  • --on <NODES> — Node selector

    Select a list of nodes to deploy to. The list is comma-separated and globs are supported. To match tags, prepend the filter by @. Valid examples:

    • host1,host2,host3 - edge-* - edge-,core- - @a-tag,@tags-can-have-*

Install NixOS on target nodes

Usage: navi install [OPTIONS]

Options:
  • --on <NODES> — Node selector

    Select a list of nodes to deploy to. The list is comma-separated and globs are supported. To match tags, prepend the filter by @. Valid examples:

    • host1,host2,host3 - edge-* - edge-,core- - @a-tag,@tags-can-have-*
  • --provisioner <PROVISIONER> — Explicitly select a provisioner to run

  • --force — Force installation even if provenance file exists

  • --unlock — Unlock the disk after installation

  • --list — List selected nodes without installing

  • --reinstall — Destroy and recreate the specific VM before installing

Start the Navi Daemon

Usage: navi daemon <COMMAND>

Subcommands:
  • start — Start the daemon server
  • status — Get daemon status

Start the daemon server

Usage: navi daemon start

Get daemon status

Usage: navi daemon status

Hive options

This chapter documents the reserved attributes of a Hive and the deployment options available on each node. Everything not listed here is ordinary NixOS configuration.

meta

meta configures the Hive as a whole.

  • meta.nixpkgs selects the package set nodes are built against. It is the one field most Hives must set.

defaults

defaults is a NixOS module merged into every node. Use it for configuration that all machines share.

deployment

The deployment attribute set on a node controls how Navi connects to it and applies its configuration.

  • deployment.targetHost is the address Navi connects to. If unset, the node name is used as the host.
  • deployment.targetUser is the user Navi connects as.
  • deployment.tags is a list of tags used to select the node with --on.
  • deployment.buildOnTarget builds the closure on the target instead of locally.

This list covers the common options. Run navi eval against a node to see the full set of deployment values it resolves to.