Machines
Every machine nspawn starts is the systemd unit systemd-nspawn@NAME.service,
registered with systemd-machined under its name. machinectl list,
machinectl status NAME, systemctl status systemd-nspawn@NAME and
journalctl -u systemd-nspawn@NAME all work on it; nspawn adds the
docker-like commands on top.
The unit also carries a drop-in, nspawn-hooks.conf, that calls nspawn around
the machine’s life: ExecStartPre prepares its network and settings,
ExecStartPost publishes its ports once it is registered, and ExecStopPost
releases everything however the machine ended. So machinectl start NAME,
systemctl enable systemd-nspawn@NAME for a machine that comes up at boot, a
program that exits on its own or a crash all behave like nspawn start and
nspawn stop. The drop-in names the nspawn binary that wrote it, which is why
nspawn belongs in /usr/local/bin or /usr/bin.
start
sudo nspawn start NAME [--network bridge|veth|host] [-p HOST:CONTAINER[/udp]]...
[--entrypoint PROGRAM] [-e VAR[=VALUE]]... [-v SOURCE:TARGET[:ro]]...
[--image-command] [--no-wait] [-- ARGUMENTS...]
start regenerates /etc/systemd/nspawn/NAME.nspawn from the machine’s
record, sets up the network it is configured for, starts the
unit through systemd and waits up to 30 seconds for the machine to register
with machined. For a booted machine it then waits, up to 20 more seconds, until
the machine’s own systemd is listening, so that a command can follow right
away; --no-wait skips that last wait. A program that runs and returns at once
is reported as such, not as a failure.
Everything given to start is remembered for the machine, so a plain
nspawn start NAME next time reuses the network, the ports, the command, the
variables and the volumes of the last run:
--networkswitches the machine between the bridge, a veth pair and the host’s network; see Networking.-p HOST:CONTAINER[/udp]publishes a port on the host, like docker. It needs the bridge network;-p noneforgets all published ports.--entrypoint,-eand the arguments after--change what an app image runs; see Command, entrypoint and environment. Boot images refuse them.-vmounts a host directory or a named volume into any kind of machine; see Volumes.
Machines are started by name. A reference (fedora:44, docker.io/x) is
refused with a hint to pull it or to create a machine from a local image.
Images that were not installed by nspawn, for example something created with
machinectl import-tar, can be started too: they get the stock template’s veth
networking, and nspawn makes sure systemd-networkd runs on the host so that the
machine actually gets an address, but none of the flags above apply to them.
Boot machines
A boot image is started with Boot=yes: systemd-nspawn runs the image’s init
as PID 1, like machinectl start does, and the machine joins the bridge with
an address that its own systemd-networkd configures.
execenters the machine’s namespaces directly, likedocker exec, so nothing is needed inside: no D-Bus, no PAM. The exit code comes back and the program is found on the machine’sPATH.shellopens machined’s login session (OpenMachineShell), which needs D-Bus inside the machine; the hub images have it. Right afterstartthe machine’s D-Bus may not be up yet, soshellretries for up to 20 seconds instead of failing.stopasks machined to power the machine off and repeats the request every two seconds until the machine is gone, for up to a minute; the repetition covers the window right after boot in which the init has not installed its signal handlers yet.--forcekills every process at once.- With volumes, a small unit mounted into the machine,
nspawn-volumes.service, holdslocal-fs.targetuntil all of them are mounted, so services find their data in place whatever the backend.
App machines
An app image is started with Boot=no and ProcessTwo=yes: nspawn’s stub init
is PID 1 and the image’s entrypoint runs as PID 2 with the environment, working
directory, user and stop signal from the OCI config. Its network namespace is
built on the host before the program starts, so the network is there from the
first instruction, as in docker. For the same reason an app on the bridge runs
without a user namespace (PrivateUsers=no), which is also docker’s default;
capabilities, seccomp and the other namespaces still apply.
execandshellenter the namespaces of the machine’s leader process, on a pseudo terminal, with the image’s environment.shellruns/bin/sh, and-u USERswitches user for both.stopsends the image’s stop signal (StopSignalin the config,SIGTERMby default) to the program itself, waits--timeoutseconds (10 by default) and kills the machine if it is still there, likedocker stop. A stop through systemd (systemctl stop, shutdown) reaches the stub init as a poweroff request, which it answers withSIGTERMto the program.- A program that exits on its own ends the machine; its network and ports are
released by the unit hooks, and
nspawn stopon it afterwards only clears what it left behind and saysNAME was not running.
Command, entrypoint and environment
What an app runs is decided exactly as with docker:
- The arguments after
--replace the image’scmdand follow itsentrypoint.nspawn start web -- nginx -Tstill runs/docker-entrypoint.shfirst, asdocker run nginx nginx -Twould. --entrypoint PROGRAMreplaces the entrypoint with one program; the arguments after--follow it.--entrypoint ""drops the entrypoint, so the arguments run alone.- Both are remembered, like the command of a docker container.
--image-commandforgets them and runs the image’s own entrypoint and cmd again. -e VAR=valueadds a variable on top of the image’s, and-e VARcopies it from the shell that runs nspawn. The later value of a variable wins,-e noneforgets them all, andexecsees the same environment as the program. Names follow the usual rules (letters, digits and_, not starting with a digit).- The working directory, the user and the stop signal come from the image. Of
docker’s
uid:gidform of the user, the uid part is used; the gid comes from the image’spasswd.
ps shows the effective command; nspawn start NAME -- true is a quick way to
check that an image runs at all.
Volumes
sudo nspawn start web -v /srv/www:/usr/share/nginx/html:ro -v pgdata:/var/lib/postgresql
-v SOURCE:TARGET[:ro] is docker’s syntax:
- An absolute
SOURCEis a host directory (or file), created when it does not exist. ASOURCEwithout a leading/is a named volume that nspawn keeps under/var/lib/nspawn/volumes/NAME, created on first use and never deleted byimages rm. Names may contain letters, digits,_,.and-. TARGETis an absolute path inside the machine, other than/. The same target cannot be mounted twice.:romounts it read-only;:rwis the default. Paths with whitespace are not supported.-vis repeatable and remembered;-v noneforgets every volume.
In machines that run with private users (booted machines on overlay and flat,
which is the default), the mount is idmapped, so root inside owns what it
writes on the host, as docker users expect. On mstack machines nspawn attaches
the volumes from the host right after the machine’s init starts, since
systemd-nspawn cannot idmap binds under managed user namespaces; that is what
nspawn-volumes.service waits for, for up to two minutes, failing visibly
otherwise. On overlay and flat the volumes come from the settings file and are
there before the init even runs.
ps
nspawn ps [-a] # same as: nspawn machines ls [-a]
MACHINE IMAGE MODE COMMAND STATE UP PID NETWORK OS
fedora-44 hub.nspawn.org/fedora:44 boot init running 2h 48213 10.99.0.2 fedora
web docker.io/library/nginx app /docker-entrypoint.sh nginx -g ... running 5m 51002 10.99.0.3 8080->80/tcp debian
db hub.nspawn.org/fedora:44 boot init stopped - - 10.99.0.4 -
ps lists every machine machined knows about; machines that nspawn did not
install show - in the image columns. -a adds the nspawn machines that are
not running. COMMAND is the effective entrypoint and arguments of an app,
and NETWORK the bridge address with the published ports, or host or
veth.
exec and shell
nspawn exec MACHINE [-u USER] COMMAND...
nspawn shell MACHINE [-u USER]
exec runs one command inside a running machine of either kind, attached to
your terminal, and exits with the command’s status, so it works in scripts and
pipelines (what goes through stdin and stdout is byte exact). The program is
looked up on the machine’s PATH, the image’s environment and the -e
variables apply, and the working directory is the image’s. Neither D-Bus nor
anything else is needed inside.
shell opens an interactive shell as root (or -u USER): machined’s login
session for booted machines, /bin/sh in the machine’s namespaces for apps.
logs
nspawn logs MACHINE [-f] [-n N] [--since WHEN] [-t] [--all] [--inside]
systemd-nspawn sends what the machine writes to its console to the journal of
systemd-nspawn@MACHINE.service, and logs reads it with journalctl. By
default only the machine’s own output is shown, from every run of the unit,
earlier ones included:
-fkeeps printing new output, starting from the last 10 lines unless-nsays otherwise.-n Nshows the last N lines;--since "10 min ago"accepts anythingjournalctl --sincedoes;-tprefixes each line with its timestamp.--allalso shows what systemd logged about the unit: start, stop, failures.--insidereads the journal of a booted machine itself (journalctl --machine), which is where the services running inside log.
logs works for stopped machines too, since the journal keeps what they wrote.
stop
sudo nspawn stop NAME [-f] [-t SECONDS] [--no-wait]
Stops the machine as described above for boot and app machines, waits until it
is gone (a minute at most for a booted machine), stops the unit so that the
image can be removed right away, clears the failure a signal-killed program
leaves on the unit, releases the firewall exceptions of a veth machine and
removes the machine’s published ports and network namespace. --no-wait
returns right after the request, without the kill after --timeout; the unit
hooks release the network when the machine ends. -f kills every process at
once, like docker kill. Stopping a machine that already ended is not an
error.