Deploying NixOS on OVH Kimsufi with nixos-anywhere

6 min read

So you’ve found yourself a cheap dedicated server on OVH Kimsufi (now branded “OVH Eco”, because marketing), and you want to run NixOS on it. OVH doesn’t offer NixOS as an install option. What they do offer is a rescue mode that gives you root SSH access to a Debian live environment. That’s all nixos-anywhere needs.

This is a walkthrough of how I deployed NixOS onto a Kimsufi server with two 4TB HGST disks, using disko for declarative partitioning and mdadm RAID-0, and the various traps I walked into along the way.

The disk layout

Kimsufi servers in the lower tiers come with two spinning rust disks. I wanted both of them in a RAID-0 for maximum usable space.

Here’s the full disko config:

{
  disko.devices = {
    disk = {
      disk0 = {
        type = "disk";
        device = "/dev/disk/by-id/ata-HGST_HUS726T4TALA6L1_V6GXVWPS";
        content = {
          type = "gpt";
          partitions = {
            esp = {
              size = "512M";
              type = "EF00";
              content = {
                type = "filesystem";
                format = "vfat";
                mountpoint = "/boot/ESP0";
                mountOptions = [ "umask=0077" ];
              };
            };
            mdadm-root = {
              size = "100%";
              content = {
                type = "mdraid";
                name = "root";
              };
            };
          };
        };
      };
      disk1 = {
        type = "disk";
        device = "/dev/disk/by-id/ata-HGST_HUS726T4TALA6L1_V6GEAMHS";
        content = {
          type = "gpt";
          partitions = {
            esp = {
              size = "512M";
              type = "EF00";
              content = {
                type = "filesystem";
                format = "vfat";
                mountpoint = "/boot/ESP1";
                mountOptions = [ "umask=0077" ];
              };
            };
            mdadm-root = {
              size = "100%";
              content = {
                type = "mdraid";
                name = "root";
              };
            };
          };
        };
      };
    };
    mdadm = {
      root = {
        type = "mdadm";
        level = 0;
        content = {
          type = "filesystem";
          format = "ext4";
          mountpoint = "/";
        };
      };
    };
  };
}

The boot config that actually works

Disko creates the RAID arrays, but it does not tell NixOS to assemble them at boot. That’s on you. If you forget this line:

boot.swraid.enable = true;

…congratulations, you’ve just installed a system that will never boot. You’ll find this out after waiting for the server to come back up, panicking, entering rescue mode, and staring at an initrd that has no idea what mdadm is.

The full boot section:

boot.loader.systemd-boot.enable = false;
boot.loader.grub = {
  enable = true;
  efiSupport = true;
  mirroredBoots = [
    { devices = [ "nodev" ]; path = "/boot/ESP0"; }
    { devices = [ "nodev" ]; path = "/boot/ESP1"; }
  ];
};
boot.loader.efi.canTouchEfiVariables = true;
boot.swraid.enable = true;

The mirroredBoots is the key piece — GRUB installs itself to both ESPs, so either disk can boot the system. (In this case the data on the RAID-0 is purely ephemeral, but for those running RAID-1 with data that matters, this mirrored boot setup is the important part — your system stays bootable even if a disk dies.) The "nodev" device means “don’t try to install to an MBR, this is pure EFI”. And swraid.enable — don’t forget it. I’ll say it again. Don’t forget it.

Running nixos-anywhere

Disable OVH monitoring first

Before you do anything else, go to the OVH panel and disable their monitoring. The kexec step replaces the running kernel in-place, which looks to OVH’s monitoring like your server just died. This triggers an automated “intervention” that can temporarily lock your netboot settings in the panel. Ask me how I know.

The install

Boot the server into OVH rescue mode from the panel. Once you can SSH in as root, from your local machine:

nix run github:nix-community/nixos-anywhere -- \
  --flake .#my-server \
  --phases kexec,disko,install \
  root@x.x.x.x

The --phases kexec,disko,install is not optional. This is the single most important flag and the hill I will die on.

Why no reboot phase

Kimsufi rescue mode is netboot-based. Every time the server reboots, it checks the OVH panel for which boot device to use. If it’s still set to “rescue”, you’re back in Debian. If nixos-anywhere includes the reboot phase (which is the default), it will happily reboot the server right back into rescue mode, not into your freshly installed NixOS.

The full workflow is:

  1. Disable OVH monitoring in the panel (if you haven’t already — seriously, do this)
  2. Boot the server into OVH rescue mode
  3. nixos-anywhere installs with --phases kexec,disko,install (no reboot)
  4. Go to the OVH panel, change netboot from “rescue” to “Boot from the hard disk”
  5. Reboot the server from the OVH panel
  6. NixOS boots

Secrets management dance

nixos-anywhere generates a new SSH host key on every run. If you’re using sops-nix (and you should be), the age key derived from that host key will change. After installation completes:

# Get the new age key
ssh-keyscan x.x.x.x 2>/dev/null | ssh-to-age

# Update .sops.yaml with the new key, then:
sops updatekeys secrets/my-server.yaml

Then on first real boot, push the updated secrets:

nix shell nixpkgs#nixos-rebuild -c nixos-rebuild switch \
  --flake .#my-server --target-host root@x.x.x.x

Verify before you deploy

Each failed boot on Kimsufi costs you real time — you have to enter rescue mode, wait for it, SSH in, re-run nixos-anywhere, wait for that, change the boot device again, reboot again. Before you commit to a deploy, check the critical bits:

nix eval .#nixosConfigurations.my-server.config.boot.swraid.enable
# should print: true

nix eval .#nixosConfigurations.my-server.config.boot.loader.grub.efiSupport
# should print: true

A 30-second nix eval can save you 30 minutes of rescue mode cycling.

The Java Web Start KVM: a portal to 2008

Kimsufi servers don’t have standard IPMI/BMC consoles. What they have is an “IP KVM” that gives you a .jnlp file — a Java Web Start launcher. In 2026. Oracle killed Java Web Start in Java 11 (2018). OVH apparently did not get the memo.

You can’t just double-click a .jnlp file on a modern system and have anything happen. You need a Java 8 runtime and a way to parse the JNLP XML, download the referenced JARs, and launch them with the right classpath. This is exactly the kind of problem Nix was born to solve.

Here’s a standalone jnlp-run.nix you can use without pulling in anyone’s entire dotfiles flake:

# jnlp-run.nix — run ancient JNLP files with nix-shell
# Usage: nix-shell jnlp-run.nix --run "jnlp-run /path/to/kvm.jnlp"
let
  pkgs = import <nixpkgs> { };
  jdk = if pkgs.stdenv.isDarwin then pkgs.zulu8 else pkgs.jdk8;
in
pkgs.mkShell {
  packages = [
    (pkgs.writeShellScriptBin "jnlp-run" ''
      export PATH="${pkgs.lib.makeBinPath [ jdk pkgs.xmlstarlet pkgs.curl pkgs.coreutils ]}"

      if [ -z "$1" ]; then
        echo "Usage: jnlp-run <file.jnlp>" >&2
        exit 1
      fi

      jnlp="$1"

      codebase=$(xml sel -t -v '/jnlp/@codebase' "$jnlp")
      jar_href=$(xml sel -t -v '/jnlp/resources/jar/@href' "$jnlp")
      jar_url="''${codebase}/''${jar_href}"

      os=$(uname -s)
      arch=$(uname -m)
      case "''${os}-''${arch}" in
        Linux-x86_64)  native_href=$(xml sel -t -v \
          '//resources[@os="Linux" and (@arch="x86_64" or @arch="amd64")]/nativelib/@href' \
          "$jnlp" 2>/dev/null) ;;
        Linux-i*86)    native_href=$(xml sel -t -v \
          '//resources[@os="Linux" and (@arch="x86" or @arch="i386")]/nativelib/@href' \
          "$jnlp" 2>/dev/null) ;;
        *)             native_href="" ;;
      esac

      mapfile -t args < <(xml sel -t -v '/jnlp/application-desc/argument' -n "$jnlp")

      tmpdir=$(mktemp -d)
      trap 'rm -rf "$tmpdir"' EXIT

      echo "Downloading $jar_url ..."
      curl -ksSL -o "$tmpdir/app.jar" "$jar_url"

      if [ -n "$native_href" ]; then
        native_url="''${codebase}/''${native_href}"
        echo "Downloading native lib $native_url ..."
        curl -ksSL -o "$tmpdir/native.jar" "$native_url"
        (cd "$tmpdir" && jar xf native.jar)
      fi

      echo "Launching with $(java -version 2>&1 | head -1) ..."
      exec java -Djava.library.path="$tmpdir" -jar "$tmpdir/app.jar" "''${args[@]}"
    '')
  ];
}

Save that to a file, download the .jnlp from the OVH panel, and:

nix-shell jnlp-run.nix --run "jnlp-run ~/Downloads/kvm_x.x.x.x.jnlp"

Nix pulls in Java 8 (Zulu on macOS, OpenJDK on Linux), xmlstarlet, and curl into an ephemeral shell. The script parses the JNLP XML, downloads the KVM applet JARs, grabs any platform-specific native libraries, and launches the whole thing. When you close the shell, it’s all gone. No system-wide Java 8 installation polluting your machine.

It’s a thoroughly modern solution to a thoroughly ancient problem. You get a VGA console view of your server that looks like it was rendered by a GPU from the Bush administration, but it works — and sometimes “it works” is all you need to figure out why your boot is hanging.

The hard-won lessons, summarized

For the skimmers and the future-me who will inevitably forget all of this:

  • --phases kexec,disko,install — always. No reboot phase. Ever.
  • Disable OVH monitoring before kexec, or enjoy your locked panel.
  • boot.swraid.enable = true — disko won’t set this for you.
  • UEFI, not BIOS — use EF00 partitions, not EF02.
  • Age keys change on every nixos-anywhere run — update sops immediately.
  • nix eval your boot config before deploying — rescue mode round-trips are not fun.
  • The KVM is Java Web Start — Nix can run it, your OS cannot.

Wrapping up

From a blank Kimsufi server in rescue mode to a fully declarative NixOS system with RAID-0, mirrored boot, WireGuard tunnels, encrypted secrets, and remote deployment — all in an evening’s work whilst sipping a good Spanish Alhambra beer. There are worse ways to spend a Friday night.