back

Installing and managing a nixos VPS

published: Jul 19, 2025

|

NOTE: IN DRAFT

In this post we are gonna install nixos on a vps, using deploy-rs for easier configuration management and sops-nix for secret management.

Part 1 - Setting up NixOS

1. Launching the server

Create a VPS instance with the configuration that suits your budget and have heard good reviews about. I’m gonna use digital ocean cause i still have some github credits left.

Before this create a ssh key to access the server.

$ ssh-keygen -t ed25519 -C "[email protected]" # replace the email with yours
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/quantinium/.ssh/id_ed25519): /home/quantinium/.ssh/id_ed25519_nixie
Enter passphrase for "/home/quantinium/.ssh/id_ed25519_nixie" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/quantinium/.ssh/id_ed25519_nixie
Your public key has been saved in /home/quantinium/.ssh/id_ed25519_nixie.pub
The key fingerprint is:
SHA256:Y2vMC2ibBZZQ9wMD2m0QcraJAMwWCBgT4YDLshjJv9A [email protected]
The key's randomart image is:
+--[ED25519 256]--+
|^=o Bo+          |
|** O * +         |
|+++ + o o        |
|=o . o   .       |
|ooo +   S        |
|o. E o + o       |
|  . + o =        |
|   o + o .       |
|    o   .        |
+----[SHA256]-----+

that’s it - this keypair is ready to be used. open the file with .pub extension where you chose to store the ssh key pair and copy it and add it to the panel in vps instance creation panel when asked.

# /home/quantinium/.ssh/id_ed25519_nixtest.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMr8qETvTGSBHbDXiOk+QOhfRvKT0wDvOwtuMEDT+Bcc [email protected]

For reference this is my configuration :-

  • Region - San Francisco (Select the one nearest to you)
  • Image - Ubuntu 24.10 x86
  • Spec - 1 vCPU | 2GB RAM | 60GB NVME Storage (make sure to have >= 2GB RAM as 1GB is not enough for kexec).
  • ssh key - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMr8qETvTGSBHbDXiOk+QOhfRvKT0wDvOwtuMEDT+Bcc [email protected]
  • Hostname - nixie

if you are also using digital ocean enable the Add improved metrics monitoring and alerting (free).

Wait for your machine to be created and you’ll be assigned the IP address of the machine. Let’s try running a command.

$ ssh -i /home/quantinium/.ssh/id_ed25519_nixie [email protected] uname -a # replace with the path of the public ssh key and server IP
Linux nixos 6.11.0-9-generic #9-Ubuntu SMP PREEMPT_DYNAMIC Mon Oct 14 13:19:59 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

We should have a working ubuntu linux vps running now.

2. Installing NixOS

Now time to cleanse the VPS and replace ubuntu with nixos. We are gonna use nixos-anywhere to install nixos on vps.

nixos-anywhere allows us to install nixos on a target machine by connecting over SSH. if the target machine is running a linux distribution, it can use kexec to boot into a temporary RAM-based nixos installer environment and uses disko to automatically partition, format and mount disk according the configuration.

before installing nixos on the system, we gotta know two details - ip_address and primary drive. so ssh into the vps

$ ssh -i /home/quantinium/.ssh/id_ed25519_nixie [email protected]# replace with the path of the public ssh key and server IP

for getting the ip either get it from your vps provider or run ip a and for getting the primary drive run lsblk

root@nixie:~# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 32:84:a5:4b:1f:40 brd ff:ff:ff:ff:ff:ff
    altname enp0s3
    altname ens3
    inet 165.22.208.155/20 brd 165.22.223.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet 10.47.0.6/16 brd 10.47.255.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::3084:a5ff:fe4b:1f40/64 scope link proto kernel_ll
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether c2:4b:b9:a2:dc:dc brd ff:ff:ff:ff:ff:ff
    altname enp0s4
    altname ens4
    inet 10.122.0.3/20 brd 10.122.15.255 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::c04b:b9ff:fea2:dcdc/64 scope link proto kernel_ll
       valid_lft forever preferred_lft forever
root@nixie:~# lsblk
NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
vda     253:0    0   50G  0 disk
├─vda1  253:1    0 48.9G  0 part /
├─vda13 253:13   0 1023M  0 part /boot
├─vda14 253:14   0    4M  0 part
└─vda15 253:15   0  106M  0 part /boot/efi
vdb     253:16   0  488K  1 disk

as we can see the ip address is 165.22.208.155 and primary drive is vda

Now on our local machine we create a configuration file for target host. we can get an example configuration here. Clone the repo to your local machine at the path where you want to store the configuration for your target host.

$ git clone https://github.com/nix-community/nixos-anywhere-examples.git
$ mv nixos-anywhere-examples/ nixie

Let examine what happening here.

we have a flake.nix that will be evaluated during system build. In it we have different configurations for various systems such as hetzer-cloud, digital ocean and a generic one for general usage. For our use, we’ll just keep the digital ocean and remove everything else. The digital ocean configuration imports several modules we’ll see further.

# flake.nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  inputs.disko.url = "github:nix-community/disko";
  inputs.disko.inputs.nixpkgs.follows = "nixpkgs";

  outputs =
    {
      nixpkgs,
      disko,
      ...
    }:
    {
      # tested with 2GB/2CPU droplet, 1GB droplets do not have enough RAM for kexec
      # nixos-anywhere --flake .#digitalocean --generate-hardware-config nixos-generate-config ./hardware-configuration.nix <hostname>
      nixosConfigurations.digitalocean = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./digitalocean.nix
          disko.nixosModules.disko
          { disko.devices.disk.disk1.device = "/dev/vda"; }
          ./configuration.nix
        ];
      };
    };
}

now lets see disk-config.nix. This is responsible for partitioning the filesystem using the disko module. You can change this file if you want to but we’ll just go with default for now. Here we have to change one tiny thing

# disk-config.nix
{ lib, ... }:
{
  disko.devices = {
    disk.disk1 = {
      device = lib.mkDefault "/dev/vda"; # replace the drive with the primary drive we noted earlier
      type = "disk";
    };
    # ...
  };
}

The digitalocean.nix contains details needed to tailor the configuration for digital ocean purposes. We don’t need to change anything it but you can read it.

We also have the configuration.nix that contains the configuration to be deployed on the target host and hardware-configuration.nix contains well nothing cause it’ll be generated during runtime to tailor to our specific host. Make some minimal changes to the configuration.nix

{
  #...
  users.users.root.initialPassword = "password"; # we will use sops-nix in future to store password as a secret
  users.users.root.openssh.authorizedKeys.keys =
  [
    # change this to your ssh key
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMr8qETvTGSBHbDXiOk+QOhfRvKT0wDvOwtuMEDT+Bcc [email protected]"
  ];
  #...
}

I forgot one thing. create a git config so we dont have mention the ip of our vps. For this cd into ~/.ssh and create a file named config and add the following content in it.

Host nixie 
    HostName 165.22.208.155
    User root
    IdentityFile ~/.ssh/id_ed25519_nixie

Now the system is ready to be deployed so just run

$ nix run nixpkgs#nixos-anywhere -- --flake .#digitalocean --generate-hardware-config nixos-generate-config ./hardware-configuration.nix nixie

We wait and stare at our text moving on our screen. If everything goes fine running the following command should show NixOS instead of Ubuntu

ssh nixie uname -a
Linux nixie 6.12.30 #1-NixOS SMP PREEMPT_DYNAMIC Thu May 22 12:29:54 UTC 2025 x86_64 GNU/Linux

So if everything goes. you have installed nixos successfully on your vps. congratulations.

note: if you get an error saying “WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!” go to ~/.ssh/known_hosts and remove the lines that have your vps ip_address in them.

We can now alter the configuration.nix and add some stuff and rebuild the target host. Add some packages to the config

environment.systemPackages = map lib.lowPrio [
  pkgs.curl
  pkgs.git
  pkgs.fastfetch
];

and rebuild and test.

$ nixos-rebuild switch --flake .#digitalocean --target-host nixie
$ ssh nixie fastfetch
          ▗▄▄▄       ▗▄▄▄▄    ▄▄▄▖             root@nixos
          ▜███▙       ▜███▙  ▟███▛             ----------
           ▜███▙       ▜███▙▟███▛              OS: NixOS 25.11 (Xantusia) x86_64
            ▜███▙       ▜██████▛               Host: Droplet (20171212)
     ▟█████████████████▙ ▜████▛     ▟▙         Kernel: Linux 6.12.30
    ▟███████████████████▙ ▜███▙    ▟██▙        Uptime: 49 mins
           ▄▄▄▄▖           ▜███▙  ▟███▛        Packages: 407 (nix-system)
          ▟███▛             ▜██▛ ▟███▛         Shell: sshd-session: root@notty
         ▟███▛               ▜▛ ▟███▛          Display (QEMU Monitor): 1024x768 @ 75 Hz in 13"
▟███████████▛                  ▟██████████▙    Terminal:
▜██████████▛                  ▟███████████▛    CPU: DO-Premium-AMD @ 2.00 GHz
      ▟███▛ ▟▙               ▟███▛             GPU: RedHat Virtio 1.0 GPU
     ▟███▛ ▟██▙             ▟███▛              Memory: 268.56 MiB / 1.93 GiB (14%)
    ▟███▛  ▜███▙           ▝▀▀▀▀               Swap: Disabled
    ▜██▛    ▜███▙ ▜██████████████████▛         Disk (/): 2.64 GiB / 48.43 GiB (5%) - ext4
     ▜▛     ▟████▙ ▜████████████████▛          Local IP (ens3): 
           ▟██████▙       ▜███▙                Locale: en_US.UTF-8
          ▟███▛▜███▙       ▜███▙
         ▟███▛  ▜███▙       ▜███▙
         ▝▀▀▀    ▀▀▀▀▘       ▀▀▀▘

Part 2 - Managing your system using deploy-rs

We can use nixos-rebuild to deploy our nixos configuration but there are many tools that expands on it and provide more features. One such tool is deploy-rs. deploy-rs has many features such as

  • rootless deployments
  • magic rollback - feature to prevent making changing that can make the target machine inaccessible.
  • multi-profile deployments - allow deploying multiples profiles independently to different users on same machine or different machines.

To get start import the deploy-rs input in the flake.nix and change the configuration as shown.

{
  # ...
  inputs.deploy-rs.url = "github:serokell/deploy-rs";

  outputs =
    { self
    , nixpkgs
    , disko
    , deploy-rs
    , ...
    }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
      deployPkgs = import nixpkgs {
        inherit system;
        overlays = [
          deploy-rs.overlays.default
          (self: super: { deploy-rs = { inherit (pkgs) deploy-rs; lib = super.deploy-rs.lib; }; })
        ];
      };
    in
    {
      nixosConfigurations.digitalocean = nixpkgs.lib.nixosSystem {
        system = system;
        modules = [
          ./digitalocean.nix
          disko.nixosModules.disko
          { disko.devices.disk.disk1.device = "/dev/vda"; }
          ./configuration.nix
        ];
      };

      deploy.nodes = {
        digitalocean = {
          hostname = "nixie";
          fastConnection = true;
          profiles = {
            system = {
              sshUser = "root";
              user = "root";
              path = deployPkgs.deploy-rs.lib.activate.nixos self.nixosConfigurations.digitalocean;
            };
          };
        };
      };

      checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
    };
}

Let see whats happening

  • we import the deploy-rs input at the top which outputs deploy-rs which we can use in the config.
  • we create nodes which define a single host/server and the profiles we want to run in it.
  • each node will have a profile in it which can be used to deploy different configurations to the same node depending on your need.
  • the last line defines a set of checks and validations for your deployment configuration.
$ deploy

This should deploy the configuration to the target host using deploy-rs now.

Part 3 - Managing Secrets using sops-nix

sops-nix is a secret management tool for nixos that allows us to encrypt and store secrets in a declarative way. It enable secrets to be decrypted at activation time and managed securely. sops-nix provides us two formats for generating keys: age and pgp. We can also create three types of keys :- - key that was made for other purposes - key made for a standalone purpose - key that was derived from a ssh key.

We create a standalone age key for encrypting and decrypting keys locally. This will making editing and updating keys locally much easier. To create a standalone key run the following commands

$ mkdir -p ~/.config/sops/age 
$ age-keygen -o ~/.config/sops/age/keys.txt 
Public key: age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl

# a public key would be printed on your console.
# if for some reason it doesnt get printed run the following command
$ age-keygen -y ~/.config/sops/age/keys.txt

create a .sops.yaml at the root of your nix configuration and add the following content and replace the user age key with key generate now.

keys:
  - &users:
    - &quantinium age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl

creation_rules:
  - path_regex: secrets/[^/]+.(yaml|json|env|ini)$
    key_groups:
    - age:
      - *quantinium

now we create a key for our server which we will deploy the configuration. this key is needed for decryption of secrets on the target machine (our VPS).

to get the public key of our target machine

# replace example.com with the ip_address of machine
$ nix-shell -p ssh-to-age --run 'ssh-keyscan example.com | ssh-to-age'

OR

# ssh into the machine using ssh nixie@ip_address and run 
$ nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'

copy the public and key and open .sops.yaml again and add the host public key

# .sops.yaml
keys:
  - &users:
    - &quantinium age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl
  - &hosts:
    - &nixie age1rgffpespcyjn0d8jglk7km9kfrfhdyev6camd3rck6pn8y47ze4sug23v3

creation_rules:
  - path_regex: secrets/[^/]+.(yaml|json|env|ini)$
    key_groups:
    - age:
      - *quantinium
      - *nixie

create a directory secrets/secrets.yaml to store your secrets using the following command. it’ll automatically open the file in the editor configured as $EDITOR

$ mkdir secrets
$ sops secrets/secrets.yaml # this command would be used to decrypt and edit the secrets in future

Now we put our super secret password in secrets.yaml but before that they need to be hashed using mkpasswd.

$ mkpasswd -s
Password: password
$y$j9T$ug6flCCQvVzpEwP8NB0g0.$R5AUc9gD4rE4jIpsFwvIPh2Z6rKVC79WmVF0IfXtBr5

reopen the secrets.yaml using sops secrets/secrets.yaml and add the following content

# secrets.yaml
nixie_password: $y$j9T$ug6flCCQvVzpEwP8NB0g0.$R5AUc9gD4rE4jIpsFwvIPh2Z6rKVC79WmVF0IfXtBr5

services:
  secret:
    message: HI

Exit the file and if you try to access the secrets.yaml, the content should be encrypted and should seem smth like this

# secrets.yaml
nixie_password: ENC[AES256_GCM,data:I5s1QFHKYiNmbF3kXIHdiyCwKlvsutBnH6pQ1W/mf+rMgBsHtOHwLRoFJ34ZCtas0L2rHX9LlB6XDBScMBsxYMyuYejtyoQJ/A==,iv:m3KjMnKH2ChpMbKuuF5qGaXskkEYc8d3R5TjIBwRC38=,tag:VZuYJh0rgx557NB7ucbebw==,type:str]
services:
    secret:
        message: ENC[AES256_GCM,data:u5U=,iv:QamOdhj7V0iqQlNv+mdBMLGsnemoFi80ppe+AFG3GaY=,tag:GbtNAsZ6wy6NE7A5AeCh/Q==,type:str]
# ...

Now lets put the secrets in our configuration. as usual add the sops-nix input in our flake.nix and add the sops modules.

# flake.nix
{
  inputs.sops-nix.url = "github:Mic92/sops-nix";
  inputs.sops-nix.inputs.nixpkgs.follows = "nixpkgs";

  outputs = { self , nixpkgs, disko, sops-nix, deploy-rs, ...}: {
      # ....
      modules = [
        #....
        ./configuration.nix
        sops-nix.nixosModules.sops
      ];
    };
  };
}

in configuration.nix add the following

# configuration.nix

  sops = {
    defaultSopsFile = ../../secrets/secrets.yaml;
    defaultSopsFormat = "yaml";

    age = {
        # import host keys as ssh keys
        sshKeyPaths = ["/etc/ssh/ssh_host_ed25519_key"];
        # this will use an age key that is already expected to be in the filesystem
        keyFile = "/var/lib/sops-nix/keys.txt";
        # if the above key is not there we create the it.
        generateKey = true;
    };
  };

Now lets use these secrets in our config. I’m gonna make the systemd service that uses the message and we can change our password to sps secret.

sops.secrets = {
  "nixie_password" = { };
  "services/secret/message" = {
    owner = "nixie";
  };
};

systemd.services."secretservice" = {
  description = "Write secret message to file";
  after = [ "sops-nix.service" ];
  wantedBy = [ "multi-user.target" ];
  script = ''
    echo "Secret path: ${config.sops.secrets."services/secret/message".path}" >> /home/nixie/secret
    echo "heres the super secret message: $(cat ${config.sops.secrets."services/secret/message".path})" >> /home/nixie/secret
  '';
  serviceConfig = {
    User = "nixie";
    WorkingDirectory = "/home/nixie";
    PermissionsStartOnly = true;
  };
};

Also add the password for the user

sops.secrets.nixie_password.neededForUsers = true;
users.mutableUsers = false;
users.users.nixie = {
  isNormalUser = true;
  extraGroups = [ "wheel" ];
  hashed
  openssh.authorizedKeys.keys = [
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMr8qETvTGSBHbDXiOk+QOhfRvKT0wDvOwtuMEDT+Bcc [email protected]"
  ];
};
    

now deploy

$ deploy

This should create a user named nixie whose password is being provided using sops and a systemd service that outputs the line at /home/nixie/secret

We can check this by

$ ssh nixie cat /home/nixie/secret
Secret path: /run/secrets/services/secret/message
heres the super secret message: HI

This concludes our endeavour to install nixos on a vps, deploy it using deploy-rs and use sops-nix for secrets management.

If you have reached till the end. Thanks for reading it and if you have any queries related to it you can message me on any of the socials.

References:-

quantinium 2025