---
title: "NixOS: The perfect distro for a production server"
description: My experience using NixOS to manage my production VPS
date: 2025-01-01
---

NixOS is a linux distribution praised for it's functional-style declarative
configuration that allows you to manage the entire system through the Nix
programming language, making your sytem reproducible and more reliable.

This is in contrast to other linux distros that use a more imperative approach
of managing the system by running commands to install packages, create
users, etc.

## NixOS as a daily-driver operating system

About half a year ago I started using Windows subsystem for linux which enabled
me to run a linux distro in the terminal, 6 weeks ago I finally decided to make
the jump and switch from windows to a full linux desktop experience.

I wanted a linux distro with a large set of latest-version packages but still
stable without risk of getting my system broken (sorry Arch).

I was considering going with Tumbleweed, a rolling release distro
by openSUSE which I was already familiar with because I was using it in WSL.

But then I remembered Nix, this weird thing that is an operating system,
programming language, and a package manager.

Nixpkgs is the the package repository with the most fresh packages (even more than AUR),
and it has a really cool benefit of being able to install all my packages
through a config file, similar to NVIM with lazy.nvim or rust with cargo.

Initially I was considering using something like Debian with nixpkgs as I heard
that was possible, but some people warned me that it's a bad idea to not use
the native package manager, so I ended up **going with NixOS as my first linux
distro on desktop**.

I followed a [great video about installing NixOS](https://www.youtube.com/watch?v=a67Sv4Mbxmc) by Vimjoyer
and was able to get it up and running.
Although I experienced some issues in the first few days, that was expected
and I was able to solve all of them.

One trap to be aware of is the nix community is sometimes too enthusiastic and
will try to convince you to do things "the nix way" like rewriting your
perfectly fine existing configs in nix or moving from stow to home manager.

In my opinion, I don't want to waste so much time configuring everything,
I am OK with only configuring system settings and packages while having things
like other dotfiles or scripts be handled the "normal linux way".

## NixOS as a production server distro

A few days ago I got my first VPS, a RS 1000 from netcup (use code
`36nc17355743780` for a $5 discount),
my goal with it was to host my website along with some other self-hosted apps
including my own server for a terminal-based social media I am working on
(which I will make a blog post about in the future).

Infrastructure as code (IaC) is the practice of managing servers through
configuration files rather than manual instructions (such as running commands),
this has several benefits such as being able to use version control and ensuring
a more reproducible server that can be easily deployed and updated using these
files.

This is where NixOS comes in and really shines, all these "nix ways" of
configuring stuff as a desktop OS seems overkill and time consuming but for a
server that's exactly the point, to be able to reproduce the entire system
from only a bunch of nix files.

## Configuring a server with NixOS

After struggling to install NixOS, I discovered [NixOS Anywhere](https://github.com/nix-community/nixos-anywhere)
which made it very easy to install remotely through SSH.

First thing was setting up proper SSH authentication and security.

Nix makes it really simple to do, just add these 2 lines to enable ssh
but disable the insecure password authentication

```nix
services.openssh.enable = true;
services.openssh.settings.PasswordAuthentication = false;
```

And to add a public key to connect with to the root user

```nix
users.users.root.openssh.authorizedKeys.keys = [
  "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO7P9K9D5RkBk+JCRRS6AtHuTAc6cRpXfRfRMg/Kyren"
];
```

If you would like to know how I generated a key that ends with `/Kyren` read
my first blog post - [The search for the perfect ssh key](http://localhost:4321/blogs/the-search-for-the-perfect-ssh-key)

### Managing secrets with sops-nix

Next, before we can get into configuring a webserver, we need a good way
to store secrets for things like SSL certifications, [sops-nix](https://github.com/Mic92/sops-nix) is a nix module
that integrates the [sops CLI tool](https://github.com/getsops/sops) into nix.

Rather than having to set up 20 different secrets for each thing, it allows
us only setup 1 secret that is stored on the server and on the admin's
system, then, any additional secret can be defined in a file that gets
encrypted automatically by sops which can safely be committed into version control.

At runtime sops will write all these files to a directory which can then be
accessed by anything that needs access to that secret, sops also makes it easy
to let specific users or groups access to a specific secret.

### Setting up automatic updates

We have added sops-nix, but how can we get it into the server?

The simple approach would be to ssh as root into the server, git clone the
repository with the config and then run

```sh
nixos-rebuild switch --flake .#default
```

_assuming you are inside the git directory where your `flake.nix` is and your
`nixosConfigurations` is called `default`_

While that certainly works, a better approach would be to listen for changes
to the git repository and automatically pull and rebuild the system.

```nix
system.autoUpgrade = {
  enable = true;
  flake = "github:kyren223/server#default";
  dates = "minutely"; # Poll interval
  flags = [
    "--no-write-lock-file" # Prevent flake.lock from upgrading
    "--option" "tarball-ttl" "0" # Required for polling below 1h
  ];
};
```

This will automatically run every minute to fetch the github repository
and rebuild the system.

But wait, what if the repository is private? that's where sops-nix comes in!

```nix
sops.secrets.github-access-token = { };
nix.extraOptions = "!include /run/secrets/github-access-token";
```

The first line defines a new `github-access-token` secret, and the second line
makes sure nix is aware of it so it can use it when trying to fetch the repo.

Of course, make sure to define the secret in your `secrets.yml` file,
The key would be `gituhb-access-token` and the value would be a github PAT
token.
It's recommended to only give the token read-only access to the repo's contents.

## Serving a static website

Now that we have a way to manage our secrets, and continious deployment of our
nix configuration, we can finally start working on a website.

The plan is to host multiple websites in the future so I'll be using [Nginx](https://nginx.org/en/)
as a reverse proxy.

```nix
services.nginx.enable = true;
services.nginx.virtualHosts."kyren.codes" = {
  useACMEHost = "kyren.codes";
  forceSSL = true;
  locations."/" = {
    index = "index.html";
    root = "/srv/website";
  };
};
```

This enables nginx, which installs it on our system then defines a new virtual
host with the domain `kyren.codes`, later if we were to host other sites, the
virtualHost can be something like `git.kyren.codes`.

The `locations."/"` sets the base url of our website to `/srv/website` which is
where I have my website's static files, and nginx will serve `index.html` as
the default page when you go to that domain, later I will explain how to setup
continious deployment to automatically update this directory on push to master.

The `useACMEHost` and `forceSSL` makes sure the server uses https, you'd need
to get a SSL certificate from [Let's Encrypt](https://letsencrypt.org/), once
again, Nix makes it really simple to do

```nix
# Define the token using nix, make sure the "acme" user can access it
sops.secrets.cloudflare-dns-api-token = { mode = "0440"; owner = "acme"; };

security.acme.acceptTerms = true;
security.acme.defaults.email = "kyren223@proton.me"; # Your email

security.acme.certs."kyren.codes" = {
  domain = "kyren.codes";
  extraDomainNames = [ "*.kyren.codes" ];
  dnsProvider = "cloudflare";
  environmentFile = "${pkgs.writeText "cf-creds" ''
    CF_DNS_API_TOKEN_FILE=${config.sops.secrets.cloudflare-dns-api-token.path}
  ''}";
  webroot = null;
};

# Allow nginx to access acme certs
users.users.nginx.extraGroups = [ "acme" ];
```

This defines a `kyren.codes` entry (which is what `useACMEHost` references)
I am using cloudflare as my DNS provider, but you can get a full list of
supported DNS providers at [this link](https://go-acme.github.io/lego/dns/), and
of course make sure to change the environment variables to match your provider.

Now if we commit and push these changes and wait about a minute for the server
to pull the changes, we should have a working static site at `kyren.codes`.

### Setting up continious deployment for the website

The way we are going to do this on the server is create a new linux user and
add to it a public ssh key that will be used for deployments.

It's recommended that the user is restricted as much as possible so in the case
of a hacker getting access to the key, they won't be able to get into other
services that are running on the server.

We can create a user declaratively like this

```nix
users.users.website = {
  isNormalUser = true;
  group = "users";
  openssh.authorizedKeys.keys = [
    # Allow an admin to manually SSH
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO7P9K9D5RkBk+JCRRS6AtHuTAc6cRpXfRfRMg/Kyren"

    # The deployment public key
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ1B/i/AQLYt6mrz0P/oUJItpvWXp7z0xHNzmcPdtwWd"
  ];
};

# Make sure the /srv/website always exists
# Give read and write permissions to the website user
# Give read permissions to the nginx group (so nginx can serve it)
systemd.tmpfiles.rules = [
  "d /srv/website 0750 website nginx"
];
```

Now all you need to do is have a github action (or similar) that builds your
website then copies the build output into the server by SSH-ing into it.
You can take a look at [my github action](https://github.com/Kyren223/website/blob/master/.github/workflows/deploy.yml)
for my website to see an example of how you can do it.

## Conclusions

NixOS helps system admins and developers achieve infrastructure as code by
allowing them to declare the entire machine's configuration using the powerful
Nix programming language.

This makes the state of production machines more consistent and reliable while
making it easy to reproduce across hundreds of machines.

Compared with the traditional approach of a debian or ubuntu server, where
system admins would have to painfully manage the state of all machines ensuring
all the dependencies, firewalls, OS settings, etc are configured properly.

Because of the difficulty, OCI containers such as docker became popular, but
they come with a cost, mainly performane due to virtualization. NixOS makes it
easy to configure programs to run on bare-metal making it a better, more
performant alternative to docker.

**Thanks for reading! wishing you a year full of code, growth and new oppurtunities!**

If you'd like to learn more about NixOS, here are some resources I found useful:

- [NixOS in production](https://github.com/Gabriella439/nixos-in-production)
- [Vimjoyer's Youtube Channel](https://www.youtube.com/@vimjoyer/videos)

Have questions or just want to chat? feel free to contact me on discord at
Kyren223 or email me at contact@kyren.codes.
