Skip to content
Go back

systemd-nspawn: The Container Runtime Already on Your Box

By SumGuy 10 min read
systemd-nspawn: The Container Runtime Already on Your Box

The Container Runtime You’ve Been Stepping Over

You probably have Docker installed. Maybe Podman. You’ve written a dozen Compose files, pulled images from registries at 2 AM, cursed at overlay filesystem weirdness at least once. And the whole time, there’s been a perfectly good container runtime sitting right there on your system, untouched, collecting dust next to systemctl and journalctl.

It’s systemd-nspawn. It shipped with systemd. systemd is on basically every Linux distro you run. You’ve been rich this whole time.

Here’s the thing: nspawn isn’t Docker. It’s not trying to be Docker. It’s a different tool for a different set of problems, and once you understand which problems those are, you’ll actually reach for it. Probably not every day. But when you need it, you’ll be glad it’s there and you don’t have to install anything.

Let’s fix the knowledge gap.


What Is systemd-nspawn, Actually

systemd-nspawn is a container manager for running full OS trees in a lightweight, namespaced environment. Think of it as a souped-up chroot that actually gives a damn about process isolation. It uses Linux namespaces (PID, mount, network, UTS, IPC) and optional cgroups — same building blocks as Docker — but without a daemon, without a registry, without an image layer system, and without any abstraction between you and the filesystem.

You point it at a directory that looks like a Linux root filesystem, and it boots it. That’s it.

Docker containers are designed around immutable, reproducible, layered images you pull from registries. nspawn containers are designed around actual OS installations you bootstrap yourself. They run full inits (systemd, if you like), they have their own /etc, they behave like a real machine. Less microservice, more “lightweight VM without the hypervisor overhead.”

The comparison table you’ve been waiting for:

FeatureDockerLXCsystemd-nspawn
Daemon requiredYes (dockerd)LXCd optionalNo
Image registryYesNoNo
Layered filesystemYes (overlay)NoNo
Full OS bootAwkwardYesYes
machinectl integrationNoPartialYes
journald integrationIndirectNoYes
Installed by defaultNoNoYes (with systemd)
Learning curveMediumMediumLow (if you know Linux)

LXC is the closest sibling — both handle full system containers. nspawn’s edge is the deep systemd integration and the zero-install story. LXC gives you more knobs; nspawn gives you less to configure wrong.


When nspawn Actually Wins

Before we get hands-on, let’s be honest about the use cases where nspawn earns its keep:

Running full system images. You want to test something in a fresh Debian environment without spinning up a VM. debootstrap + nspawn gets you there in under two minutes.

Debian-style chroots that don’t suck. Classic chroot breaks in weird ways (no /proc, no /dev, no network). nspawn mounts all of that automatically. It’s the chroot that actually works.

Dev environments with full inits. Sometimes you need the full init system running — systemd services, timers, the whole stack — not just a single-process container. nspawn handles this naturally. Docker does it with hacks.

Sysadmin tooling and rescue operations. Testing configs, package installs, service setups — all in an isolated container on your real hardware, with real access to block devices if you need it.

Immutable host setups. If you’re running an immutable distro (like a hardened server where you really don’t want extra daemons), nspawn lets you run containers with zero runtime overhead.

Ephemeral scratch environments. Flag --ephemeral gives you a throwaway copy of your container tree. Boot it, mess it up, throw it away. Next boot is clean.

Where nspawn struggles: distributed workloads, microservices, pulling prebuilt images from Docker Hub, anything that assumes the Docker API, and teams where everyone needs to read a Compose file and immediately understand what’s running.


Bootstrap a Debian Container in 5 Minutes

Enough theory. Let’s build one.

You need debootstrap and systemd-nspawn (the latter ships with systemd-container on most distros).

Terminal window
# Debian/Ubuntu
sudo apt install debootstrap systemd-container
# Arch
sudo pacman -S arch-install-scripts
# nspawn is already in systemd

Bootstrap a minimal Debian Bookworm root filesystem:

Terminal window
sudo debootstrap --arch=amd64 bookworm /var/lib/machines/mydebian http://deb.debian.org/debian

This pulls a minimal Debian root into /var/lib/machines/mydebian. That directory is the standard home for nspawn containers — machinectl looks here automatically.

Boot it:

Terminal window
sudo systemd-nspawn -D /var/lib/machines/mydebian -b

The -b flag boots the container (runs the init system). Without it, you get a shell but no systemd, no services. You’ll land at a login prompt. Default root password is empty — just hit Enter.

You’re in. It feels like a VM but it’s sharing your kernel. No hypervisor. No second kernel. Just namespaces.

Set a root password so you can log in properly:

Terminal window
# Inside the container
passwd root

Exit with Ctrl+]]] (three times, quickly).


machinectl: Managing Containers Like a Sysadmin

machinectl is the management interface for nspawn containers registered in /var/lib/machines/. Think of it like docker ps / docker start / docker stop but for system containers.

Terminal window
# List all registered machines (running and stopped)
machinectl list-images
# Start a container as a background service
sudo machinectl start mydebian
# See what's running
machinectl list
# Open a shell in a running container
sudo machinectl shell mydebian
# Stop it
sudo machinectl stop mydebian
# Pull basic status
machinectl status mydebian

Under the hood, machinectl start creates a systemd service unit on the fly (systemd-nspawn@mydebian.service). The container runs as a proper systemd-managed service, which means systemctl status, journald logs, resource limits via cgroups — all the good stuff.

Terminal window
# Check the container's journal from the host
sudo journalctl -M mydebian -f
# Resource usage
systemctl status systemd-nspawn@mydebian.service

That journal integration alone is worth a lot. Try getting clean, host-accessible logs out of a Docker container at 2 AM when something’s on fire. You’ll appreciate this.


Persistent vs Ephemeral Mode

By default, containers in /var/lib/machines/ are persistent. Changes survive reboots. This is great for long-lived dev environments or test boxes.

Sometimes you want the opposite — boot a clean copy, break it, throw it away. That’s --ephemeral:

Terminal window
sudo systemd-nspawn -D /var/lib/machines/mydebian --ephemeral -b

nspawn creates a temporary snapshot (using btrfs snapshots if your filesystem supports it, or a plain copy otherwise), boots it, and discards it when you exit. Your original container stays untouched.

This is genuinely useful for:

If you’re on btrfs, snapshots are instant and nearly free on storage. On ext4, it’s a full copy — still fine for small containers, but mind your disk space.


Networking: Getting Your Container Online

By default, nspawn containers share the host network namespace — they see everything the host sees. Convenient for quick access, terrible for isolation.

Virtual Ethernet Pair (--network-veth)

Creates a dedicated veth pair — one end in the container, one on the host. The container gets a private address.

Terminal window
sudo systemd-nspawn -D /var/lib/machines/mydebian -b --network-veth

Inside the container, you’ll see a host0 interface. Configure it with networkd or a static IP. On the host, the container side appears as ve-mydebian.

To give it internet access, you need NAT:

Terminal window
# On the host
sudo iptables -t nat -A POSTROUTING -s 192.168.150.0/24 -j MASQUERADE
sudo sysctl net.ipv4.ip_forward=1

Or let systemd-networkd handle it — which leads us to bridge mode.

Bridge Mode (--network-bridge)

Connect the container to an existing bridge interface. Cleaner for home labs where you already have br0 or similar.

Terminal window
sudo systemd-nspawn -D /var/lib/machines/mydebian -b --network-bridge=br0

The container gets a real interface on your bridge, behaves like another machine on your LAN. Assign a static IP inside the container and treat it like a VM.

Shared Directories

Pass directories from the host into the container with --bind:

Terminal window
# Read-write bind mount
sudo systemd-nspawn -D /var/lib/machines/mydebian -b \
--bind=/home/kingpin/projects:/projects
# Read-only bind mount
sudo systemd-nspawn -D /var/lib/machines/mydebian -b \
--bind-ro=/etc/localtime:/etc/localtime

Useful for sharing source code, configuration, or making the container’s timezone match the host.


.nspawn Unit Files: Persistent Configuration

Passing flags on the CLI every time gets old fast. nspawn supports per-container config files at /etc/systemd/nspawn/<name>.nspawn:

container.nspawn
[Exec]
# Boot the full init system
Boot=yes
# Set the container hostname
Hostname=mydebian
[Network]
# Use a virtual ethernet pair for network isolation
VirtualEthernetExtra=yes
Bridge=br0
[Files]
# Bind mount host projects directory
Bind=/home/kingpin/projects:/projects
# Read-only localtime sync
BindReadOnly=/etc/localtime:/etc/localtime

Save this as /etc/systemd/nspawn/mydebian.nspawn and now machinectl start mydebian picks it up automatically — no CLI flags needed.

You can also enable the container to start at boot:

Terminal window
sudo machinectl enable mydebian
sudo machinectl start mydebian

That’s a systemd service enable under the hood (systemd-nspawn@mydebian.service). It will start on every boot, show up in machinectl list, and be manageable with every tool you already know.


A Quick Word on Quadlets

If you’re running Docker-compatible workloads and want the same “managed by systemd” experience, check out Quadlets — systemd-native unit files for Podman containers. They let you define a container as a .container file in /etc/containers/systemd/ and manage it with systemctl the same way you’d manage nspawn machines.

nspawn is for full OS trees. Quadlets are for OCI containers (your standard Docker images). Together they cover basically every container use case without needing a persistent daemon.

The pattern is: nspawn for “I need a full Debian environment” and Quadlets for “I need to run nginx in a container but I want systemd to babysit it.”


When to Reach for nspawn vs Docker

Here’s the honest rule of thumb:

Use nspawn when:

Use Docker (or Podman) instead when:

The short version: Docker is for applications. nspawn is for systems. They’re not competing for the same jobs.

Your 2 AM self will appreciate knowing which one to reach for before things get complicated.


Try It Right Now

You don’t need to install anything. You just need debootstrap (one package) and a few minutes:

Terminal window
# Install debootstrap (systemd-container is probably already present)
sudo apt install debootstrap systemd-container # Debian/Ubuntu
# Bootstrap a fresh Debian container
sudo debootstrap bookworm /var/lib/machines/sandbox http://deb.debian.org/debian
# Boot it ephemerally — changes are discarded on exit
sudo systemd-nspawn -D /var/lib/machines/sandbox --ephemeral -b
# Or boot it persistently and manage it with machinectl
sudo machinectl start sandbox
sudo machinectl shell sandbox

That’s a full Debian environment, isolated from your host, no daemon, no overlay filesystem, no Docker socket. It’s been on your machine this whole time. You just hadn’t opened the drawer.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Coolify vs Dokploy: Self-Hosted Vercel for People Who Don't Trust Vercel
Next Post
Distroless Images: When Minimal Goes Too Far

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts