Minimal NixOS systemd-nspawn containers

3 min read Original article ↗

As I’ve mentioned before on this blog, I really like using Nix and NixOS to manage and deploy my systems. Now that I’m doing more projects than ever, I’ve started experiencing some friction with the monolithic deployment model of NixOS—I have multiple git repositories in which I’m developing various projects and I don’t want to constantly redeploy my main DigitalOcean droplet NixOS config every time I make a small change.

NixOS uses systemd which provides a great lightweight container tool called systemd-nspawn. When you run a container with systemd-nspawn it registers it with systemd-machined which makes it integrate well with the rest of the system:

  1. systemctl --recursive status will show the full service tree including services inside containers.
  2. Automatic resolution of the container name to its IP address through nss-mymachines.
  3. systemd-run -M <container> -- command will execute a command inside the container.
  4. journalctl -M <container> -u service shows the logs of a service running inside the container.

The list goes on, where possible systemd integrates the host and guest.

NixOS has support for systemd-nspawn container management built-in, but this is not quite what I wanted: you either declaratively specify your containers (which means it’s still monolithic) or you imperatively manage them with the perl script which has some annoying limitations (like the length of the container name) and it does a bunch of scripting to set up the virtual network.

Perhaps these features were added after the above was introduced to NixOS, but the built-in systemd-nspawn@.service sets some very sane defaults that virtualizes users and networking. If both the host and guest are using systemd-networkd and systemd-resolved then networking will magically just work, the host will set up a DHCP server on a virtual ethernet connection that the guest uses to configure its networking.

My own minimal version

I want to declaratively specify the containers that are on a system while imperatively deploying new versions of them. What I’ve landed on is some simple configuration that augments the built-in systemd-nspawn@.service and a script that deploys a NixOS configuration from a flake to the host system. I’ve put the modules and script on GitHub, the README shows how to use it. The host module is straightforward and the guest module just sets some modern defaults.

The only hump I had in getting the automagic networking setup is NixOS’ firewall blocking DHCP requests on the automatic virtual ethernet interface, hopefully this PR can be merged soon though a workaround is possible (it’s in the README).

Result

I can now deploy multiple projects from my laptop to DigitalOcean VM by running nixos-deploy-container which is super snappy since it only builds a minimal container. For me this strikes a great balance between declarative and imperative workflows.

We will probably adopt something very similar to this at my job to manage our growing number of internal services. As we grow I’m contemplating modifying nomad-driver-nspawn to run NixOS system closures directly, which would be an awesome NixOS-native way to orchestrate containers.

Mar 2026