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:
buildevaluates and builds only.pushalso copies the closure to the target.switchactivates immediately and makes the change persist across reboots.bootmakes 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:
buildevaluates and builds only.pushalso copies the closure to the target.switchactivates immediately and makes the change persist across reboots.bootmakes 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:
- Provisioning, creating the machine through Terraform.
- Installation, putting NixOS onto it with nixos-anywhere.
- Disk decryption, unlocking encrypted volumes in initrd.
- 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:
terraformSecretscontrols the injection. It defaults totrue; set it tofalseto keep the credentials out of Terraform's variables.apiKeyVariableandsecretKeyVariableset the variable names Terraform sees. They default toporkbun_api_keyandporkbun_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:
apiKeyCommandoutputs the API key.userCommandoutputs 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:
enableturns on unlock support for the node.passwordCommandis a local command whose output is the passphrase. Keeping it a command means the passphrase is never written into the Hive.remoteCommandis 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, anduseroverride the initrd SSH endpoint when it differs from the running system's.portdefaults to 2222.forceHwLinkroutes 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:
destDiris the directory on the host; it defaults to/run/keys.nameis the file name, defaulting to the attribute name.user,group, andpermissionsset ownership and mode, defaulting toroot:rootand0600.
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-activationuploads 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:
navi↴navi apply↴navi apply-local↴navi build↴navi eval↴navi upload-keys↴navi exec↴navi list↴navi repl↴navi nix-info↴navi tui↴navi serial↴navi ssh↴navi disk-unlock↴navi facts↴navi facts derive↴navi provision↴navi install↴navi daemon↴navi daemon start↴navi daemon status↴
navi
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 machinesapply-local— Apply configurations on the local machinebuild— Build configurations but not push to remote hostseval— Evaluate an expression using the complete configurationupload-keys— Upload keys to remote hostsexec— Run a command on remote machineslist— List available hosts and their configurationrepl— Start an interactive REPL with the complete configurationnix-info— Show information about the current Nix installationtui— Start an interactive TUI dashboardserial— Connect to node serial consolessh— SSH into a hostdisk-unlock— Unlock a disk on a remote hostfacts— Manage persistent factsprovision— Provision infrastructure for nodesinstall— Install NixOS on target nodesdaemon— 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 commandsPasses --show-trace to Nix commands
-
--impure— Allow impure expressionsPasses --impure to Nix commands
-
--nix-option <NAME>— Passes an arbitrary option to Nix commandsThis only works when building locally.
-
--color <WHEN>— When to colorize the outputBy 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:
autoPossible values:
auto: Detect automaticallyalways: Always display colorsnever: Never display colors
navi apply
Apply configurations on remote machines
Usage: navi apply [OPTIONS] [GOAL]
Arguments:
-
<GOAL>— Deployment goalSame 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
switchis the default goal unless--rebootis passed, in which casebootis the default.Default value:
switchPossible values:
build: Build the configurations onlypush: Push the closures onlyswitch: Make the configuration the boot default and activate nowboot: Make the configuration the boot defaulttest: Activate the configuration, but don't make it the boot defaultdry-activate: Show what would be done if this configuration were activatedupload-keys: Only upload keys
Options:
-
--eval-node-limit <LIMIT>— Evaluation node limitLimits 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 limitLimits 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
.gcrootsin the directory the Hive configuration is located. -
-v,--verbose— Be verboseDeactivates the progress spinner and prints every line of output.
-
--no-keys— Do not upload keysBy default, Navi will upload secret keys set in
deployment.keysbefore deploying the new profile on a node. To upload keys without building or deploying the rest of the configuration, usenavi upload-keys. -
--reboot— Reboot nodes after activationReboots nodes after activation and waits for them to come back up.
-
--no-substitute— Do not use substitutesDisables the use of substituters when copying closures to the remote host.
-
--no-gzip— Do not use gzipDisables the use of gzip when copying closures to the remote host.
-
--build-on-target— Build the system profiles on the target nodesIf 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 nodesdeployment.replaceUnknownProfilessettingIf
deployment.replaceUnknownProfilesis set for a target, using this switch will treatdeployment.replaceUnknownProfilesas though it was set totrueand perform unknown profile replacement. -
--install-bootloader— Install bootloaderIf 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 tostreaming, the experimental streaming evaluator (nix-eval-jobs) will be used and nodes will be evaluated in parallel.This is an experimental feature.
Default value:
chunkedPossible values:
chunked,streaming -
--on <NODES>— Node selectorSelect 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-*
navi apply-local
Apply configurations on the local machine
Usage: navi apply-local [OPTIONS] [GOAL]
Arguments:
-
<GOAL>— Deployment goalSame as the targets for switch-to-configuration. "push" is noop in apply-local.
Default value:
switchPossible values:
build: Build the configurations onlypush: Push the closures onlyswitch: Make the configuration the boot default and activate nowboot: Make the configuration the boot defaulttest: Activate the configuration, but don't make it the boot defaultdry-activate: Show what would be done if this configuration were activatedupload-keys: Only upload keys
Options:
-
--sudo— Attempt to escalate privileges if not run as root -
-v,--verbose— Be verboseDeactivates the progress spinner and prints every line of output.
-
--no-keys— Do not deploy keysDo not deploy secret keys set in
deployment.keys. By default, Navi will deploy keys set indeployment.keysbefore activating the profile on this host. -
--install-bootloader— Install bootloaderIf enabled, the bootloader will be installed during activation. This is equivalent to
NIXOS_INSTALL_BOOTLOADER=1. -
--node <NODE>— Override the node name to use
navi build
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 limitLimits 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 limitLimits 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
.gcrootsin the directory the Hive configuration is located. -
-v,--verbose— Be verboseDeactivates the progress spinner and prints every line of output.
-
--no-keys— Do not upload keysBy default, Navi will upload secret keys set in
deployment.keysbefore deploying the new profile on a node. To upload keys without building or deploying the rest of the configuration, usenavi upload-keys. -
--reboot— Reboot nodes after activationReboots nodes after activation and waits for them to come back up.
-
--no-substitute— Do not use substitutesDisables the use of substituters when copying closures to the remote host.
-
--no-gzip— Do not use gzipDisables the use of gzip when copying closures to the remote host.
-
--build-on-target— Build the system profiles on the target nodesIf 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 nodesdeployment.replaceUnknownProfilessettingIf
deployment.replaceUnknownProfilesis set for a target, using this switch will treatdeployment.replaceUnknownProfilesas though it was set totrueand perform unknown profile replacement. -
--install-bootloader— Install bootloaderIf 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 tostreaming, the experimental streaming evaluator (nix-eval-jobs) will be used and nodes will be evaluated in parallel.This is an experimental feature.
Default value:
chunkedPossible values:
chunked,streaming -
--on <NODES>— Node selectorSelect 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-*
navi eval
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
navi upload-keys
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 limitLimits 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 limitLimits 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
.gcrootsin the directory the Hive configuration is located. -
-v,--verbose— Be verboseDeactivates the progress spinner and prints every line of output.
-
--no-keys— Do not upload keysBy default, Navi will upload secret keys set in
deployment.keysbefore deploying the new profile on a node. To upload keys without building or deploying the rest of the configuration, usenavi upload-keys. -
--reboot— Reboot nodes after activationReboots nodes after activation and waits for them to come back up.
-
--no-substitute— Do not use substitutesDisables the use of substituters when copying closures to the remote host.
-
--no-gzip— Do not use gzipDisables the use of gzip when copying closures to the remote host.
-
--build-on-target— Build the system profiles on the target nodesIf 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 nodesdeployment.replaceUnknownProfilessettingIf
deployment.replaceUnknownProfilesis set for a target, using this switch will treatdeployment.replaceUnknownProfilesas though it was set totrueand perform unknown profile replacement. -
--install-bootloader— Install bootloaderIf 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 tostreaming, the experimental streaming evaluator (nix-eval-jobs) will be used and nodes will be evaluated in parallel.This is an experimental feature.
Default value:
chunkedPossible values:
chunked,streaming -
--on <NODES>— Node selectorSelect 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-*
navi exec
Run a command on remote machines
Usage: navi exec [OPTIONS] <COMMAND>...
Arguments:
-
<COMMAND>— Command to runIt'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 limitLimits 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 verboseDeactivates the progress spinner and prints every line of output.
-
--on <NODES>— Node selectorSelect 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-*
navi list
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 keyPossible values:
tag: One section per tag (hosts with several tags appear in each)provisioner: One section per provisionerlink: One section per connection link (SSH, GCP, ...)
navi repl
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
navi nix-info
Show information about the current Nix installation
Usage: navi nix-info
navi tui
Start an interactive TUI dashboard
Usage: navi tui [OPTIONS]
Options:
-
-p,--parallel <PARALLEL>— Limit the maximum number of hosts to be deployed in parallelDefault value:
10 -
--no-daemon— Disable daemon and force local executionDefault value:
false
navi serial
Connect to node serial console
Usage: navi serial <NODE>
Arguments:
<NODE>— The node to connect to
navi ssh
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 selectorSelect 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-*
navi disk-unlock
Unlock a disk on a remote host
Usage: navi disk-unlock <HOST>
Arguments:
<HOST>— Host to unlock
navi facts
Manage persistent facts
Usage: navi facts <COMMAND>
Subcommands:
derive— Derive facts from the Nix configuration (Pre-computation)
navi facts derive
Derive facts from the Nix configuration (Pre-computation)
Usage: navi facts derive [FILTERS]...
Arguments:
<FILTERS>— Filter facts to generate (glob patterns)
navi provision
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 selectorSelect 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-*
navi install
Install NixOS on target nodes
Usage: navi install [OPTIONS]
Options:
-
--on <NODES>— Node selectorSelect 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
navi daemon
Start the Navi Daemon
Usage: navi daemon <COMMAND>
Subcommands:
start— Start the daemon serverstatus— Get daemon status
navi daemon start
Start the daemon server
Usage: navi daemon start
navi daemon status
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.nixpkgsselects 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.targetHostis the address Navi connects to. If unset, the node name is used as the host.deployment.targetUseris the user Navi connects as.deployment.tagsis a list of tags used to select the node with--on.deployment.buildOnTargetbuilds 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.