Provisioning remote machines via SSH#

It is possible to replace any Linux installation with a NixOS configuration on running systems using nixos-anywhere and disko.

Introduction#

In this tutorial, you will deploy a NixOS configuration to a running computer.

What will you learn?#

You’ll learn how to

  • Specify a minimal NixOS configuration with a declarative disk layout and SSH access

  • Check that a configuration is valid

  • Deploy and update a NixOS configuration on a remote machine

What do you need?#

For a successful unattended installation, ensure for the target machine that:

  • It is a QEMU virtual machine running Linux

    • With kexec support

    • On the x86-64 or aarch64 instruction set architecture (ISA)

    • With at least 1 GB of RAM

    This may also be a live system booted from USB, such as the NixOS installer.

  • The IP address is configured automatically with DHCP

  • You can login via SSH

    • With public key authentication (prefered), or password

    • As user root or another user with sudo permissions

The local machine only needs a working Nix installation.

We call the target machine target-machine in this tutorial. Replace it with the actual hostname or IP address.

Prepare the environment#

Create a new project directory and enter it with your shell:

mkdir remote
cd remote

Specify dependencies on nixpkgs, disko, and nixos-anywhere:

$ nix-shell -p npins
[nix-shell:remote]$ npins init
[nix-shell:remote]$ npins add github nix-community disko
[nix-shell:remote]$ npins add github nix-community nixos-anywhere

Create a new file shell.nix which provides all needed tooling using the pinned dependencies:

let
  sources = import ./npins;
  pkgs = import sources.nixpkgs {};
in

pkgs.mkShell {
  nativeBuildInputs = with pkgs; [
    npins
    nixos-anywhere
    nixos-rebuild
  ];
  shellHook = ''
    export NIX_PATH="nixpkgs=${sources.nixpkgs}:nixos-config=$PWD/configuration.nix"
  '';
}

Now exit the temporary environment and enter the newly specified one:

[nix-shell:remote]$ exit
$ nix-shell

This shell environment is ready to use well-defined versions of Nixpkgs with nixos-anywhere and nixos-rebuild.

Important

Run all following commands in this environment.

Create a NixOS configuration#

The new NixOS configuration will consist of the general system configuration and a disk layout specification.

The disk layout in this example describes a single disk with a master boot record (MBR) and EFI system partition (ESP) partition, and a root file system that takes all remaining available space. It will work on both EFI and BIOS systems.

Create a new file single-disk-layout.nix with the disk layout specification:

 1{ ... }:
 2
 3{
 4  disko.devices.disk.main = {
 5    type = "disk";
 6    content = {
 7      type = "gpt";
 8      partitions = {
 9        MBR = {
10          priority = 0;
11          size = "1M";
12          type = "EF02";
13        };
14        ESP = {
15          priority = 1;
16          size = "500M";
17          type = "EF00";
18          content = {
19            type = "filesystem";
20            format = "vfat";
21            mountpoint = "/boot";
22          };
23        };
24        root = {
25          priority = 2;
26          size = "100%";
27          content = {
28            type = "filesystem";
29            format = "ext4";
30            mountpoint = "/";
31          };
32        };
33      };
34    };
35  };
36}

Create the file configuration.nix, which imports the disk layout definition and specifies which disk to format:

Tip

If you don’t know the target disk’s device identifier, list all devices on the target machine with lsblk:

$ ssh target-machine lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
sda      8:0    0   256G  0 disk
├─sda1   8:1    0 248.5G  0 part /nix/store
│                                /
└─sda2   8:2    0   7.5G  0 part [SWAP]
sr0     11:0    1  1024M  0 rom

In this example, the disk name is sda. The block device path is then /dev/sda. Note that value for later.

 1{ modulesPath, ... }:
 2
 3let
 4  diskDevice = "/dev/sda";
 5  sources = import ./npins;
 6in
 7{
 8  imports = [
 9    (modulesPath + "/profiles/qemu-guest.nix")
10    (sources.disko + "/module.nix")
11    ./single-disk-layout.nix
12  ];
13
14  disko.devices.disk.main.device = diskDevice;
15
16  boot.loader.grub = {
17    devices = [ diskDevice ];
18    efiSupport = true;
19    efiInstallAsRemovable = true;
20  };
21
22  services.openssh.enable = true;
23
24  users.users.root.openssh.authorizedKeys.keys = [
25    "<your SSH key here>"
26  ];
27
28  system.stateVersion = "24.11";
29}

Important

Replace /dev/sda with your disk block device path.

Replace the <your SSH key here> string with the SSH public key that you want to use for future logins as user root.

Detailed explanation

The diskDevice variable in the let block defines the path of the disk block device:

3let
4  diskDevice = "/dev/sda";
5  sources = import ./npins;
6in

It is used to set the target for the partitioning and formatting as described in the disk layout specification. It is also used in the boot loader configuration to make it bootable on both legacy BIOS as well as UEFI systems:

14  disko.devices.disk.main.device = diskDevice;
15
16  boot.loader.grub = {
17    devices = [ diskDevice ];
18    efiSupport = true;
19    efiInstallAsRemovable = true;
20  };

The qemu-guest.nix module makes this system compatible for running inside a QEMU virtual machine:

 8  imports = [
 9    (modulesPath + "/profiles/qemu-guest.nix")
10    (sources.disko + "/module.nix")
11    ./single-disk-layout.nix
12  ];

From a disk layout specification, the disko library generates a partitioning script and the portion of the NixOS configuration that mounts the partitions accordingly at boot time. The first line imports the library, the second line applies the disk layout:

 8  imports = [
 9    (modulesPath + "/profiles/qemu-guest.nix")
10    (sources.disko + "/module.nix")
11    ./single-disk-layout.nix
12  ];

Test the disk layout#

Check that the disk layout is valid:

nix-build -E "((import <nixpkgs> {}).nixos [ ./configuration.nix ]).installTest"

This command runs the complete installation in a virtual machine by building a derivation in the installTest attribute provided by the disko module.

Deploy the system#

To deploy the system, build the configuration and the corresponding disk formatting script, and run nixos-anywhere using the results:

Important

Replace target-host with the hostname or IP address of your target machine.

toplevel=$(nixos-rebuild build --no-flake)
diskoScript=$(nix-build -E "((import <nixpkgs> {}).nixos [ ./configuration.nix ]).diskoScript")
nixos-anywhere --store-paths "$diskoScript" "$toplevel" root@target-host

Note

If you don’t have public key authentication: Set the environment variable SSH_PASS to your password then append the --env-password flag to the nixos-anywhere command.

nixos-anywhere will now log into the target system, partition, format, and mount the disk, and install the NixOS configuration. Then, it reboots the system.

Update the system#

To update the system, run npins and re-deploy the configuration:

npins update nixpkgs
nixos-rebuild switch --no-flake --target-host root@target-host

nixos-anywhere is not needed any more, unless you want to change the disk layout.

Next steps#

References#