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:
| Feature | Docker | LXC | systemd-nspawn |
|---|---|---|---|
| Daemon required | Yes (dockerd) | LXCd optional | No |
| Image registry | Yes | No | No |
| Layered filesystem | Yes (overlay) | No | No |
| Full OS boot | Awkward | Yes | Yes |
| machinectl integration | No | Partial | Yes |
| journald integration | Indirect | No | Yes |
| Installed by default | No | No | Yes (with systemd) |
| Learning curve | Medium | Medium | Low (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).
# Debian/Ubuntusudo apt install debootstrap systemd-container
# Archsudo pacman -S arch-install-scripts# nspawn is already in systemdBootstrap a minimal Debian Bookworm root filesystem:
sudo debootstrap --arch=amd64 bookworm /var/lib/machines/mydebian http://deb.debian.org/debianThis 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:
sudo systemd-nspawn -D /var/lib/machines/mydebian -bThe -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:
# Inside the containerpasswd rootExit 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.
# List all registered machines (running and stopped)machinectl list-images
# Start a container as a background servicesudo machinectl start mydebian
# See what's runningmachinectl list
# Open a shell in a running containersudo machinectl shell mydebian
# Stop itsudo machinectl stop mydebian
# Pull basic statusmachinectl status mydebianUnder 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.
# Check the container's journal from the hostsudo journalctl -M mydebian -f
# Resource usagesystemctl status systemd-nspawn@mydebian.serviceThat 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:
sudo systemd-nspawn -D /var/lib/machines/mydebian --ephemeral -bnspawn 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:
- Testing package installs without committing to them
- Running potentially destructive automation
- Giving junior devs a sandbox that can’t permanently break anything
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.
sudo systemd-nspawn -D /var/lib/machines/mydebian -b --network-vethInside 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:
# On the hostsudo iptables -t nat -A POSTROUTING -s 192.168.150.0/24 -j MASQUERADEsudo sysctl net.ipv4.ip_forward=1Or 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.
sudo systemd-nspawn -D /var/lib/machines/mydebian -b --network-bridge=br0The 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:
# Read-write bind mountsudo systemd-nspawn -D /var/lib/machines/mydebian -b \ --bind=/home/kingpin/projects:/projects
# Read-only bind mountsudo systemd-nspawn -D /var/lib/machines/mydebian -b \ --bind-ro=/etc/localtime:/etc/localtimeUseful 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:
[Exec]# Boot the full init systemBoot=yes# Set the container hostnameHostname=mydebian
[Network]# Use a virtual ethernet pair for network isolationVirtualEthernetExtra=yesBridge=br0
[Files]# Bind mount host projects directoryBind=/home/kingpin/projects:/projects# Read-only localtime syncBindReadOnly=/etc/localtime:/etc/localtimeSave 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:
sudo machinectl enable mydebiansudo machinectl start mydebianThat’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:
- You need a full OS environment, not a single-process container
- You want to test system-level changes (init scripts, service configs, kernel module loading, package interactions)
- You’re building a chroot-style dev or build environment
- Your host is immutable or daemon-free by design
- You care about deep systemd/journald integration
- You want ephemeral scratch boxes you can trash without touching the base image
- You don’t have Docker installed and don’t want to install it
Use Docker (or Podman) instead when:
- You’re deploying application workloads — web apps, APIs, databases
- You need to pull prebuilt images from registries
- Your team expects a Dockerfile and Compose file
- You’re running microservices or anything distributed
- You want the full ecosystem (Docker Hub, build caching, multi-stage builds, volumes with lifecycle management)
- You’re targeting Kubernetes eventually
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:
# Install debootstrap (systemd-container is probably already present)sudo apt install debootstrap systemd-container # Debian/Ubuntu
# Bootstrap a fresh Debian containersudo debootstrap bookworm /var/lib/machines/sandbox http://deb.debian.org/debian
# Boot it ephemerally — changes are discarded on exitsudo systemd-nspawn -D /var/lib/machines/sandbox --ephemeral -b
# Or boot it persistently and manage it with machinectlsudo machinectl start sandboxsudo machinectl shell sandboxThat’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.