Run a Command Safely in a systemd-nspawn Container
systemd-nspawn drops you into a container about as fast as chroot, but with real namespace isolation behind it. This guide runs one command inside an existing OS tree, then a full boot, then shows how to make it disposable and repeatable. Allow 15 minutes for an existing OS tree, or longer if you still need to build one. It uses systemd 255.4, package version 255.4-1ubuntu8.17, as installed on the reference machine.
The route
Jump straight to the step you need, or tick off Done means at the end.
1. Check the tool and choose an OS tree
systemd-nspawn needs a directory containing a Linux filesystem, including /etc/os-release or /usr/lib/os-release when booting it. The directory can come from a distribution bootstrap tool, an image unpacked by your normal provisioning process, or an existing machine under /var/lib/machines/. It is not a general-purpose sandbox for an arbitrary empty directory.
$ systemd-nspawn --version
systemd 255 (255.4-1ubuntu8.17)
$ test -f /path/to/container/etc/os-release && echo 'OS metadata found'
Use an absolute placeholder path until you have replaced it. Most container setup and mounting operations require elevated privileges, so the examples that start a container use sudo. The command inside the container is still governed by the container's own user database and filesystem.
2. Run one command with a stub init
For an arbitrary command, use --as-pid2. Without it, nspawn runs the selected command as PID 1, and PID 1 has special duties (reaping orphaned processes, handling signals) that ordinary utilities do not implement. With --as-pid2, a stub init stays as PID 1 and runs your command as PID 2.
$ sudo systemd-nspawn \
--directory=/path/to/container \
--machine=demo \
--as-pid2 \
/usr/bin/printf 'container says: %s\n' 'hello'
container says: hello
The command's output is the useful result. Check the exit status immediately if a script will act on it:
$ printf 'exit status: %s\n' "$?"
exit status: 0
That status belongs to the command just run. Do not insert a separate inspection command before printing it.
Tip
The --machine= name is used for registration and the initial container hostname, so keep it short. It also avoids awkward network interface names later.
3. Open a shell or boot the whole system
Omit a command and skip --boot, and nspawn starts a shell instead: useful for an interactive repair or inspection session.
$ sudo systemd-nspawn --directory=/path/to/container --machine=demo
root@demo:~# cat /etc/os-release
root@demo:~# exit
Use --boot when the directory contains a complete system and you want its init program as PID 1:
$ sudo systemd-nspawn --boot --directory=/path/to/container --machine=demo
Warning
Do not combine --boot and --as-pid2. The first asks nspawn to find and run an init system; the second is for a single payload command. The interactive command line defaults to single-command mode, while the [email protected] template uses boot mode and other defaults, so a service invocation will not behave like the command you tested by hand.
4. Make a run disposable before testing changes
Use --ephemeral for a temporary snapshot of the container filesystem. Anything the run changes is discarded the moment nspawn exits:
$ sudo systemd-nspawn \
--ephemeral \
--directory=/path/to/container \
--as-pid2 \
/usr/bin/sh -c 'touch /tmp/throw-away; test -f /tmp/throw-away && echo temporary-ok'
temporary-ok
- Snapshot support matters. It is more efficient on Btrfs or suitable XFS than on traditional filesystems; the command must still be able to create its runtime mounts.
- Verify after exit, not before. Treat the original tree as unchanged only once the process has finished, and check that a file you expected to keep was not touched.
- There is no recovery step. Anything deliberately written only inside an ephemeral run is gone for good once the container exits.
5. Add a persistent, reviewable configuration
Put administrator-owned settings in /etc/systemd/nspawn/, named after the machine. The file is optional and uses an INI-like syntax.
Checkpoint
A file placed beside an image or in its parent directory is treated more cautiously; potentially privileged settings in it are ignored. That stops an image vendor or a downloaded image from silently requesting host access.
$ sudo install -d /etc/systemd/nspawn
$ sudoedit /etc/systemd/nspawn/demo.nspawn
For a command container, enter:
[Exec]
Boot=no
Parameters=/usr/bin/printf nspawn-config-ok
Environment=DEMO_MODE=review
WorkingDirectory=/tmp
Now the machine name alone selects both the root tree and the settings file:
$ sudo systemd-nspawn --machine=demo
nspawn-config-ok
Parameters= is a whitespace-separated command line, with quotes available for arguments containing spaces. Command-line options normally override settings-file values; --settings=override reverses that precedence, and --settings=trusted allows every setting from any discovered file to take effect. Avoid both modes unless you have reviewed every setting source yourself.
6. Limit persistence and host reach
--read-only mounts the container root read-only, though additional bind and temporary mounts can still be writable. For a disposable stateful test, --volatile=state leaves the OS tree read-only and puts /var/ on temporary storage; --volatile=overlay gives the whole tree a temporary writable overlay instead. Either way, the changes disappear when the container terminates.
$ sudo systemd-nspawn \
--read-only \
--volatile=state \
--directory=/path/to/container \
--as-pid2 \
/usr/bin/sh -c 'printf "state is temporary\n"; test -w /var'
Warning
A bind mount is an explicit host-to-container access path, so review it like a privilege decision. Use --private-network when the payload should have its own network namespace, or --network-veth when you also want a virtual Ethernet link; both require the host's networking setup and privileges. Use --bind-ro=/host/path:/container/path when the payload only needs to read.
Common traps
- Boot refused. If nspawn rejects booting the tree, check both
/etc/os-releaseand/usr/lib/os-release. A missing OS metadata file is a safety check failing, not proof the directory is a valid image. - Command behaves oddly as PID 1. Rerun it with
--as-pid2. Save--bootfor an actual init system. - Command not found. Check that its path exists inside the container, not just on the host; the root filesystem changes what
/usr/bin/...means. - Setting seems ignored. Check the file's location and the active
--settingsmode. Settings beside an image do not automatically gain privileged effect. - No network inside. That may be deliberate. Interactive nspawn does not imply a veth; the service template has different defaults.
Done means
- Version checked. You confirmed the installed systemd-nspawn version and picked a real OS tree.
- One-shot command works. It runs with
--as-pid2and returns the status you expect. - Boot mode is deliberate. You use
--bootonly when the tree contains an init system. - Throwaway tests stay throwaway. Anything that must leave no trace uses
--ephemeralor a volatile mode. - Config is reviewed. Persistent settings live in a checked
/etc/systemd/nspawn/machine.nspawnfile, and every bind or network option has been treated as a host-access decision.