Installing NixOS on a OVH VPS

Written on: 2026-05-27

Why

I want to install NixOS on my VPS that is serving my static site[1] because it lets me manage basically the entire system declaratively and reproducibly, and centralizes entire system configuration in a single place, which is really convenient.

However, OVH doesn’t provide a way to directly install NixOS: it is not one of the options to select for the OS install during VPS setup and there’s no option to mount an installer image either and install that way.

Step 1: Boot into the NixOS installer kexec image

The main thing we do to get around OVH’s limitations is this. The rest of the install is a standard manual NixOS install.

Kexec is a mechanism in the Linux kernel to load a new kernel bypassing the BIOS/UEFI boot process. This new kernel is loaded directly into memory which lets us edit partitions safely.

First, login into your VPS as root (or enter a root shell using su -). Then ensure that your local SSH key is added to the root user, either by manually appending your local pubkey to /root/.ssh/authorized_keys on the server or by doing ssh-copy-id -i path/to/local/key.pub root@<VPS_IP> locally if you’ve set up a root password on the server. This is because the kexec script copies over the root user’s authorized keys to the new system and we would be unable to login to the installer.

Next, we download the kexec image and boot into it by running the following commands. The following lines were taken from https://github.com/nix-community/nixos-images#kexec-tarballs. Replace latest by nixos-YY.MM to get a specific installer.

curl -L https://github.com/nix-community/nixos-images/releases/latest/download/nixos-kexec-installer-noninteractive-x86_64-linux.tar.gz | tar -xzf- -C /root
/root/kexec/run

You should get some output like this. After this, the VPS will reboot.

<eases/latest/download/nixos-kexec-installer-noninteractive-x86_64-linux.tar.gz | tar -xzf- -C /root
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100  418M  100  418M    0     0  43.3M      0  0:00:09  0:00:09 --:--:-- 43.2M
[RESCUE] root@vps-0183d86f:~ $ ls
kexec
[RESCUE] root@vps-0183d86f:~ $ /root/kexec/
ip     kexec  run
[RESCUE] root@vps-0183d86f:~ $ /root/kexec/run
+ kexec_extra_flags=
+ [ 0 -gt 0 ]
+ init=/nix/store/f1y8crmaniipyybfzwc3m31swhvm0379-nixos-system-nixos-installer-kexec-25.11pre-git/init
+ kernelParams=nouveau.modeset=0 console=tty0 console=ttyS0,115200 zswap.enabled=1 zswap.max_pool_percent=50 zswap.compressor=zstd zswap.zpool=zsmalloc root=fstab loglevel=4 lsm=landlock,yama,bpf
+ readlink -f /root/kexec/run
+ dirname /root/kexec/run
+ SCRIPT_DIR=/root/kexec
+ TMPDIR=/root/kexec mktemp -d
+ INITRD_TMP=/root/kexec/tmp.2fK7wH287T
+ cd /root/kexec/tmp.2fK7wH287T
+ trap cleanup EXIT
+ mkdir -p ssh
+ extractPubKeys /root
+ home=/root
+ key=/root/.ssh/authorized_keys
+ test -e /root/.ssh/authorized_keys
+ grep -o \(\(ssh\|ecdsa\|sk\)-[^ ]* .*\) /root/.ssh/authorized_keys
+ key=/root/.ssh/authorized_keys2
+ test -e /root/.ssh/authorized_keys2
+ grep -o \(\(ssh\|ecdsa\|sk\)-[^ ]* .*\) /root/.ssh/authorized_keys2
+ test -n
+ test -n
+ test -e /etc/ssh/authorized_keys.d/root
+ test -n
+ test -e /etc/ssh/ssh_host_ecdsa_key
+ cp -a /etc/ssh/ssh_host_ecdsa_key ssh
+ test -e /etc/ssh/ssh_host_ecdsa_key.pub
+ cp -a /etc/ssh/ssh_host_ecdsa_key.pub ssh
+ test -e /etc/ssh/ssh_host_ed25519_key
+ cp -a /etc/ssh/ssh_host_ed25519_key ssh
+ test -e /etc/ssh/ssh_host_ed25519_key.pub
+ cp -a /etc/ssh/ssh_host_ed25519_key.pub ssh
+ test -e /etc/ssh/ssh_host_rsa_key
+ cp -a /etc/ssh/ssh_host_rsa_key ssh
+ test -e /etc/ssh/ssh_host_rsa_key.pub
+ cp -a /etc/ssh/ssh_host_rsa_key.pub ssh
+ /root/kexec/ip --json addr
+ /root/kexec/ip -4 --json route
+ /root/kexec/ip -6 --json route
+ [ -f /etc/machine-id ]
+ cp /etc/machine-id machine-id
+ find .
+ cpio -o -H newc
+ gzip -9
17 blocks
+ kexecSyscallFlags=
+ + uname -r
sort -c -V
+ printf %s\n 6.1 6.1.0-42-amd64
+ kexecSyscallFlags=--kexec-syscall-auto
+ sh -c '/root/kexec/kexec' --load '/root/kexec/bzImage'   --kexec-syscall-auto      --initrd='/root/kexec/initrd' --no-checks   --command-line 'init=/nix/store/f1y8crmaniipyybfzwc3m31swhvm0379-nixos-system-nixos-installer-kexec-25.11pre-git/init nouveau.modeset=0 console=tty0 console=ttyS0,115200 zswap.enabled=1 zswap.max_pool_percent=50 zswap.compressor=zstd zswap.zpool=zsmalloc root=fstab loglevel=4 lsm=landlock,yama,bpf'
+ echo machine will boot into nixos in 6s...
machine will boot into nixos in 6s...
+ test -e /dev/kmsg
+ exec
[RESCUE] root@vps-0183d86f:~ $ Read from remote host <VPS_IP>: Connection reset by peer

After this, logout and run ping <VPS_IP> to check if the VPS is done booting up. When it starts responding to ping, SSH into root@<VPS_IP> again. You should see the NixOS installer prompt, something like this:

> ssh root@<VPS_IP>

[root@nixos-installer:~]#

Step 2: Partitioning and formatting disks

We need to partition the hardrive for our new system. I chose to keep it simple with a 1MiB BIOS boot partition and the rest goes to an XFS filesystem root partition. While I’m using gdisk in this article, there are various CLI partitioning tools available to you in the installer such as parted, fdisk, cfdisk, and cgdisk.

First, run lsblk to identify the disk you want to install stuff on.

[root@nixos-installer:~]# lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
loop0    7:0    0 384.1M  0 loop /nix/.ro-store
loop1    7:1    0    36K  1 loop /run/nixos-etc-metadata
sda      8:0    0   2.9G  0 disk
└─sda1   8:1    0   2.9G  0 part
sdb      8:16   0    75G  0 disk
├─sdb1   8:17   0     1G  0 part
└─sdb2   8:18   0    74G  0 part

Then, run gdisk on that disk.

[root@nixos-installer:~]# gdisk /dev/sdb
GPT fdisk (gdisk) version 1.0.10

Partition table scan:
  MBR: protective
  BSD: not present
  APM: not present
  GPT: present

Found valid GPT with protective MBR; using GPT.

Command (? for help): ?
b	back up GPT data to a file
c	change a partition's name
d	delete a partition
i	show detailed information on a partition
l	list known partition types
n	add a new partition
o	create a new empty GUID partition table (GPT)
p	print the partition table
q	quit without saving changes
r	recovery and transformation options (experts only)
s	sort partitions
t	change a partition's type code
v	verify disk
w	write table to disk and exit
x	extra functionality (experts only)
?	print this menu

Starting fresh with a new GPT partition table.

Command (? for help): o
This option deletes all partitions and creates a new protective MBR.
Proceed? (Y/N): y

Creating a new 1 MiB partition with GUID ef02 for the BIOS boot partition.

Command (? for help): n
Partition number (1-128, default 1):
First sector (34-157286366, default = 2048) or {+-}size{KMGTP}:
Last sector (2048-157286366, default = 157284351) or {+-}size{KMGTP}: +1M
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): ef02
Changed type of partition to 'BIOS boot partition'

Creating a new partition for the filesystem root that covers the rest of the disk.

Command (? for help): n
Partition number (2-128, default 2):
First sector (34-157286366, default = 4096) or {+-}size{KMGTP}:
Last sector (4096-157286366, default = 157284351) or {+-}size{KMGTP}:
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300):
Changed type of partition to 'Linux filesystem'

Printing the partition table as a sanity check.

Command (? for help): p
Disk /dev/sdb: 157286400 sectors, 75.0 GiB
Model: QEMU HARDDISK
Sector size (logical/physical): 512/512 bytes
Disk identifier (GUID): EB4DA20D-5B57-492D-90BF-48442E4A581D
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 157286366
Partitions will be aligned on 2048-sector boundaries
Total free space is 4029 sectors (2.0 MiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048            4095   1024.0 KiB  EF02  BIOS boot partition
   2            4096       157284351   75.0 GiB    8300  Linux filesystem

Writing the partition table to disk and exiting.

Command (? for help): w

Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING
PARTITIONS!!

Do you want to proceed? (Y/N): y
OK; writing new GUID partition table (GPT) to /dev/sdb.
The operation has completed successfully.

Formatting the root partition with XFS and giving it a label. Leave the BIOS boot partition unformatted.

[root@nixos-installer:~]# mkfs.xfs -L nixos /dev/sdb2
meta-data=/dev/sdb2              isize=512    agcount=4, agsize=4915008 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=1, sparse=1, rmapbt=1
         =                       reflink=1    bigtime=1 inobtcount=1 nrext64=1
         =                       exchange=0   metadir=0
data     =                       bsize=4096   blocks=19660032, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1, parent=0
log      =internal log           bsize=4096   blocks=16384, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
         =                       rgcount=0    rgsize=0 extents
         =                       zoned=0      start=0 reserved=0
Discarding blocks...Done.

Step 3: Creating the system configuration

After partitioning and formatting is done, mount the FS root to /mnt.

[root@nixos-installer:~]# mount /dev/sdb2 /mnt

Run nixos-generate-config to create an initial config.

[root@nixos-installer:~]# nixos-generate-config --root /mnt --flake

Use nix shell to get vim (or nano) in a shell as it is not provided by default in the kexec image. Then edit the generated files. One quirk of the kexec images is that they don’t set the nixpkgs search path which leads to errors on non-flake nix commands. This might be problematic if you don’t want to use flakes.

[root@nixos-installer:~]# nix shell nixpkgs#vim
[root@nixos-installer:~]# vim -p /mnt/etc/nixos/*.nix

Change the channel in flake.nix from nixos-unstable to nixos-25.11.

Modify configuration.nix. The things I changed were (important things bolded):

  1. Bootloader, use GRUB since this is a BIOS system and not a UEFI one.

      # Use the GRUB 2 boot loader.
      boot.loader.grub.enable = true;
      # boot.loader.grub.efiSupport = true;
      # boot.loader.grub.efiInstallAsRemovable = true;
      # boot.loader.efi.efiSysMountPoint = "/boot/efi";
      # Define on which hard drive you want to install Grub.
      boot.loader.grub.device = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-0-0-1"; # or "nodev" for efi only

    Run ls -Alh /dev/disk/by-id to get the HWID for the boot physical disk. This is more stable across reboots than using drive letters like sda.

    [root@nixos-installer:~]# ls  -Alh /dev/disk/by-id/
    total 0
    lrwxrwxrwx 1 root root  9 May 25 07:27 scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-0-0-0 -> ../../sda
    lrwxrwxrwx 1 root root 10 May 25 07:27 scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-0-0-0-part1 -> ../../sda1
    lrwxrwxrwx 1 root root  9 May 25 07:27 scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-0-0-1 -> ../../sdb
    lrwxrwxrwx 1 root root 10 May 25 07:27 scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-0-0-1-part1 -> ../../sdb1
    lrwxrwxrwx 1 root root 10 May 25 07:27 scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-0-0-1-part2 -> ../../sdb2
  2. Networking configuration

    Define a hostname.

      networking.hostName = "shaurya-vps-2"; # Define your hostname.

    Use DHCP to automatically get IP assignments and other network config.

      networking.useDHCP = true;

    Disable NetworkManager since this is a server.

      # networking.networkmanager.enable = true;
  3. Console config

      # Select internationalisation properties.
      i18n.defaultLocale = "en_US.UTF-8";
      console = {
        font = "Lat2-Terminus16";
        keyMap = "us";
      #   useXkbConfig = true; # use xkb.options in tty.
      };
  4. Add a non-privileged user

      # Define a user account. Don't forget to set a password with ‘passwd’.
      users.users.shaurya = {
        isNormalUser = true;
        extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
      };
  5. Install some packages

      # List packages installed in system profile.
      # You can use https://search.nixos.org/ to find more packages (and options).
      environment.systemPackages = with pkgs; [
        vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
        wget
      ];

    Pro tip: Use https://searchix.ovh for a better and more comprehensive search interface.

  6. SSH config. Without this, you will be unable to use SSH to login to the server.

    Enable OpenSSH.

      services.openssh.enable = true;

    Add local SSH keys to the remote users for more convenient access.

      users.users.root.openssh.authorizedKeys.keys = ["YOUR LOCAL SSH PUBLIC KEY"];
      users.users.shaurya.openssh.authorizedKeys.keys = ["YOUR LOCAL SSH PUBLIC KEY"];

My final configuration.nix and flake.nix looked like this.

Step 4: Actual installation

Now, we run nixos-install. Due to either a minor error in my configuration or a bug in Nix, the command failed the first time I ran it but was successful on the second run. Once again, running it without the flake option errors due to a missing nixpkgs search path in the kexec image.

[root@nixos-installer:~]# nixos-install --root /mnt --flake /mnt/etc/nixos#nixos
warning: creating lock file "/mnt/etc/nixos/flake.lock":
• Added input 'nixpkgs':
    'github:NixOS/nixpkgs/b77b3de8775677f84492abe84635f87b0e153f0f?narHash=sha256-nOesoDCiXcUftqbRBMz9tt4blI5PvljMWbm3kuCA%2B0s%3D' (2026-05-22)
building the flake in path:/mnt/etc/nixos?lastModified=1779695002&narHash=sha256-Lk%2B3Ai10dLd8m8GjK%2B9CsMYSavTWR33eABOGOKZ35gU%3D...
nix: ../flake.cc:37: nix::StorePath nix::flake::copyInputToStore(nix::EvalState&, nix::fetchers::Input&, const nix::fetchers::Input&, nix::ref<nix::SourceAccessor>): Assertion `!originalInput.getNarHash() || storePath == originalInput.computeStorePath(*state.store)' failed.
/run/current-system/sw/bin/nixos-install: line 226:   900 Aborted                    (core dumped) nix "${flakeFlags[@]}" build "$flake#$flakeAttr.config.system.build.toplevel" --store "$mountPoint" --extra-substituters "$sub" "${verbosity[@]}" "${extraBuildFlags[@]}" "${lockFlags[@]}" --out-link "$outLink"

[root@nixos-installer:~]# nixos-install --root /mnt --flake /mnt/etc/nixos#nixos
building the flake in path:/mnt/etc/nixos?lastModified=1779695066&narHash=sha256-5mYC%2B7ZrfaEOtKMDcxsGJ95z7YXWcxdeA24Re436UWw%3D...
warning: ignoring substitute for '/nix/store/3hgg7pr65imdrifqqh3flg3arvkc2r22-bash-5.3p3' from 'local', as it's not signed by any of the keys in 'trusted-public-keys'

...

warning: ignoring substitute for '/nix/store/rpw5s22rbyajiz90jx8r2d3d76w2bqqr-linux-headers-static-6.16.7' from 'local', as it's not signed by any of the keys in 'trusted-public-keys'
warning: ignoring substitute for '/nix/store/k0q0wirzk1z3zvmfrajp5wbbh6dk5vrb-dash-0.5.13.2' from 'local', as it's not signed by any of the keys in 'trusted-public-keys'
warning: download buffer is full; consider increasing the 'download-buffer-size' setting
installing the boot loader...
setting up /etc...
updating GRUB 2 menu...
installing the GRUB 2 boot loader on /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-0-0-1...
Installing for i386-pc platform.
Installation finished. No error reported.
setting up /etc...
setting up /etc...
setting root password...
New password:
Retype new password:
passwd: password updated successfully
installation finished!

NixOS should now be installed on the VPS’s disk! Reboot so that the newly installed system can boot up and run.

[root@nixos-installer:~]# reboot

Now, wait for it to finish booting up using the ping trick mentioned earlier and then you should be able to SSH in.

> ssh root@<VPS_IP>
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:7ANWw23oBlfpVAqncl5cy9oLY619cZAwT57+rAG0HSI.
Please contact your system administrator.
Add correct host key in /home/shaurya/.ssh/known_hosts to get rid of this message.
Offending RSA key in /home/shaurya/.ssh/known_hosts:45
Host key for <VPS_IP> has changed and you have requested strict checking.
Host key verification failed.

Oh right, the SSH host key of the server will be regenerated on install so we need to delete the old host key from our local system.

> ssh-keygen -R 15.204.253.234

And now it works.

> ssh root@<VPS_IP>
Last login: Mon May 25 08:19:14 2026 from <CLIENT_IP>

[root@shaurya-vps-2:~]# exit
logout
Connection to <VPS_IP> closed.
> ssh shaurya@<VPS_IP>

[shaurya@shaurya-vps-2:~]$

Voila!

Mistakes I made and fixes

I made quite a few mistakes along the way which led to failure several times before I was able to successfully install. I am including details of these mistakes and how I dealt with them in this section. While this wasted quite a bit of time, it was also an educational experience and I hope you might learn something from this too.

Not adding my local key to the root user before running kexec

This meant that I was unable to SSH into the installer system and had to reboot from the OVH web UI, then login again and restart the process.

Thinking that the server used UEFI instead of BIOS

I assumed that the server would be using something modern and new like UEFI instead of something old like BIOS. So, I setup a 1GiB EFI boot partition and changed the configuration to use systemd-boot instead of GRUB. This meant that the server wasn’t able to boot into the new install and hanged at boot time.

So I had to repartition and reformat the disk and change the configuration to use GRUB again. First, I rebooted into rescue mode from the OVH control panel web UI. Since the main disk of the new system wasn’t bootable, a normal reboot wouldn’t work without a reinstall from the control panel and rebooting into rescue mode meant that the config changes I made weren’t clobbered.

I did the kexec stuff again. I then copied over the configuration.nix and flake.nix files to the installer’s home directory so I didn’t have to re-add the various changes I made to the configuration. Then I partitioned and formatted the disk again, generated the config, replaced the non-hardware-specific parts with files I had copied over earlier. Then I changed the config to use GRUB instead of systemd-boot and proceeded with the rest of the install.

Messing up the network configuration

I messed up the network configuration by using a modified form of static IP assignments as suggested in this article I was loosely following for the install. This led to broken networking on the VPS which meant I wasn’t able to SSH into it post-install. I think what I did wrong was mix up IPv4 and IPv6 nameservers or gateways but I’m not sure what the problem exactly was. I found out that the install was successful because I checked the virtual KVM access that the OVH control panel provides and it showed a NixOS login screen. To fix this, I used the KVM to login to the machine and then modified the configuration to use DHCP that lets the server get its network configuration from OVH’s router. After that, everything worked a nixos-rebuild later.

Credits

I referenced this article by Raghav Sood heavily during the installation process. Thanks also go to the maintainers of nix-community/nix-image for providing kexec images.


  1. I know, a VPS is somewhat overkill for a simple static site but I do plan on hosting some more dynamic content eventually like perhaps a Forgejo instance or a Vaultwarden server.