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/runYou 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 peerAfter 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 partThen, 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 menuStarting 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): yCreating 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 filesystemWriting 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 /mntRun nixos-generate-config to create an initial config.
[root@nixos-installer:~]# nixos-generate-config --root /mnt --flakeUse 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/*.nixChange the channel in flake.nix from nixos-unstable to nixos-25.11.
Modify configuration.nix. The things I changed were (important things bolded):
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 onlyRun
ls -Alh /dev/disk/by-idto get the HWID for the boot physical disk. This is more stable across reboots than using drive letters likesda.[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 -> ../../sdb2Networking 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;Console config
# Select internationalisation properties. i18n.defaultLocale = "en_US.UTF-8"; console = { font = "Lat2-Terminus16"; keyMap = "us"; # useXkbConfig = true; # use xkb.options in tty. };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. };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.
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:~]# rebootNow, 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.234And 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.
- 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. ↩