How to Use systemd-nspawn Containers on Linux
Creating and running a lightweight systemd-nspawn container — useful for isolated testing environments without a full container runtime's overhead.
systemd-nspawn provides namespace-based container isolation built directly into systemd — using the same namespace primitives that underlie Docker and Podman, without needing a separate container runtime installed.
Step 1: create a root filesystem for the container
sudo dnf --releasever=39 --installroot=/var/lib/machines/mycontainer install systemd passwd dnf fedora-release vim-minimal -y
Or, on Debian-based systems:
sudo debootstrap stable /var/lib/machines/mycontainer http://deb.debian.org/debian
Either produces a minimal, bootable root filesystem in the standard /var/lib/machines/ location machinectl expects.
Step 2: boot the container
sudo systemd-nspawn -D /var/lib/machines/mycontainer -b
-b boots the container’s own init system (systemd) rather than just running a single command inside it — the container behaves like a lightweight, independently-booting system.
Step 3: manage it with machinectl instead of raw nspawn commands
sudo machinectl start mycontainer
sudo machinectl shell mycontainer
sudo machinectl stop mycontainer
machinectl is the higher-level tool for managing nspawn containers as persistent, named machines — closer to docker start/exec/stop in spirit.
Step 4: list running containers
machinectl list
Step 5: set resource limits via systemd’s own resource control
systemctl set-property machine-mycontainer.scope MemoryMax=512M CPUQuota=50%
Because nspawn containers run as systemd scopes, the same resource-control mechanisms covered in cgroups apply directly — no separate resource-limiting subsystem needed.
Step 6: share a directory between host and container
sudo systemd-nspawn -D /var/lib/machines/mycontainer --bind=/host/data:/data -b
Step 7: enable the container to start automatically at boot
sudo machinectl enable mycontainer
Step 8: remove a container you no longer need
sudo machinectl remove mycontainer
Why nspawn is worth knowing alongside Docker/Podman, not instead of them
nspawn containers boot a full init system and behave much more like lightweight VMs than application containers — genuinely useful for testing a full distribution environment, building packages in isolation, or running a legacy service that expects a real init system, scenarios where Docker’s single-process-per-container model is a worse fit. It’s a different tool for a different job, not a competing choice for the container-orchestration workloads Docker and Podman are built around.
Step 9: run a fully throwaway container for one-off testing
sudo systemd-nspawn -D /var/lib/machines/mycontainer --ephemeral -b
--ephemeral boots from a temporary copy-on-write snapshot of the specified root filesystem rather than the filesystem itself, and discards every change automatically the moment the container stops — the base image is never modified no matter what happens inside the running container. This is genuinely useful for reproducible testing (verifying an install script against a known-clean base every single run) or for confidently trying something destructive inside the container without any cleanup step needed afterward, since there’s nothing left to clean up once it exits.
Step 10: give a container its own isolated virtual network interface
sudo systemd-nspawn -D /var/lib/machines/mycontainer --network-veth -b
By default, nspawn containers share the host’s network namespace entirely — --network-veth instead creates a veth pair (the same mechanism network namespace testing uses) connecting the container to the host through a private virtual link, giving the container its own network stack rather than direct visibility into the host’s. machinectl and systemd-networkd cooperate to set up addressing on this private link automatically in most current distribution configurations, and this isolation is genuinely important for any container scenario where you don’t want the containerized environment able to see or interfere with the host’s actual network configuration and listening services directly.
Step 11: fetch a ready-made image instead of building a root filesystem by hand
sudo machinectl pull-tar https://cloud-images.example.org/debian-13.tar.xz mycontainer
Rather than manually running debootstrap or dnf --installroot as shown in Step 1, machinectl pull-tar (or pull-raw for disk images) downloads and verifies a pre-built image directly, registering it under /var/lib/machines/ ready to boot — a meaningfully faster path to a working container when a suitable pre-built image already exists, reserving the manual bootstrap approach for cases needing a genuinely custom or unusual base image.
Step 12: take a snapshot before making risky changes inside a container
sudo machinectl clone mycontainer mycontainer-backup
Because a container’s root filesystem is just an ordinary directory tree (or a disk image) under /var/lib/machines/, machinectl clone makes a full copy under a new name in one command — a cheap, disposable checkpoint to fall back to before testing something destructive inside the container, without needing any of the copy-on-write snapshot infrastructure a filesystem like Btrfs would otherwise provide for this. If /var/lib/machines is itself backed by Btrfs, machinectl automatically uses a fast, space-efficient reflink-based clone instead of a full byte-for-byte copy — worth knowing about specifically because it changes clone speed and disk usage dramatically without requiring anything different in the command you actually type.
Step 13: log into a container without a full shell session
sudo machinectl login mycontainer
machinectl login opens a genuine getty-style login prompt against the container, authenticating as a real user inside it, distinct from machinectl shell’s more direct root-equivalent shell access covered in Step 3 — useful specifically when you want to test the container’s actual login path (PAM configuration, a specific unprivileged user’s environment) rather than simply getting a working shell inside it as quickly as possible.
Related:
- Linux Namespaces: The Kernel Primitive Behind Every Container
- Control Groups (cgroup v2) Explained: Limiting and Accounting for Resources
Sources: