What is systemd-nspawn?
systemd
is known as INIT system, which is used in many unix and linux distributions. systemd-nspawn
is available as a part of default systemd package and if it is not available, install the following packages:
- systemd-container (MAIN)
- bridge-utils (OPTIONAL - For managing networks)
systemd-nspawn
can be used to run raw images or docker images without installing additional software tools and it is controlled by systemd
, with the help of namespaces all the networks and logs are separated in our host itself. This post is not about what docker can and cannot do, it’s more about how to use systemd-nspawn
to achieve containerization without having to worry about extra tooling.
I have seen many people calling this as a replacement for chroot
, which is true and it can be much more than that is my opinion. Before, proceeding further, please make backup of your system, incase of issues. Though this is purely a precaution, we don’t want to risk losing data.
Before we proceed:
- HOST or host means your machine, the machine that you’re using to create containers
- CONTAINER or container means the actual containers created using the host
Tools - we’ll be using:
- systemd-nspawn
- machinectl
- debootstrap (OPTIONAL - wants a fresh installation)
- cloud-init (OPTIONAL - only if you choose cloud based image)
Pick your favourite OS or Image:
I chose ubuntu, which is relevant to my usecase. Choose from Ubuntu Cloud Images .
Don’t rush to download it, we will be doing it using machinectl
command. You can run the following commands as root
(if you’re daring!)
$ machinectl pull-tar http://cloud-images.ubuntu.com/xenial/current/xenial-server-cloudimg-amd64-root.tar.gz xenial
$ machinectl pull-tar [LINK] [CONTAINER NAME]
......PROGRESS NOTIFICATION.........
Poof! where did my image go? Don’t worry, it’s safe in /var/lib/machines/xenial
directory. By default, all images pulled using machinectl goto that directory.
Power on your first specialised container:
$ systemd-nspawn -D /var/lib/machines/alpine -U -M xenial
Spawning container xenial on /var/lib/machines/xenial.
Press ^] three times within 1s to kill container.
Selected user namespace base 706150400 and range 65536.
root@xenial:~#
Anything you do from now on is inside the container and make sure you’re not doing it on your actual machine.
Add a new user:
root@xenial:~# adduser test-user
......Normal User addition steps...........
Add instance name to hosts file:
root@xenial:~# echo "127.0.0.1 xenial" >> /etc/hosts
# If this is not done, then we'll keep getting an error `unable to resolve hostname: xenial` when executing commands.
Adding a default network interface inside the container (Optional)
If you don’t want to complicate your container setup by adding separate network for the container, then you can skip and proceed to the usage part. By default, the container talks to the host through a host0
network interface, we need to add it the interfaces to autostart, when the container boots.
When we say network couple of things come into picture,
- private subnet or host network (for host network, skip this section)
- Wired Network or Wireless network in host
Bridged Network with a Wired Connection in host
- For a wired network, it is straight forward, you create a bridge network between your container’s virtual network and the wired connection using
brctl
(bridge-utils) in your host machine
$ brctl addbr virxen
$ brctl show virxen
bridge name bridge id STP enabled interfaces
virxen 8000.000000000000 no
$ brctl addif [host internet connected wired interface]
- If you do have a
dhcp service
running in your container, then you can add this in your container’s network interfaces, which automatically gets an ip assigned for this container.
# systemd-nspawn -D /var/lib/machines/alpine -U -M xenial
Spawning container xenial on /var/lib/machines/xenial.
Press ^] three times within 1s to kill container.
Selected user namespace base 706150400 and range 65536.
root@xenial:~# echo 'auto host0' >> /etc/network/interfaces
root@xenial:~# echo 'iface host0 inet dhcp' >> /etc/network/interfaces
- Now create a file named
xenial.nspawn
or any other name you’ve created your container with the following contents:
[Exec]
# Writable bind mounts don't with user namespacing
PrivateUsers=no
[Network]
VirtualEthernet=yes
Bridge=virxen
- Make a directory named
nspawn
in/etc/systemd
directory - Copy the above file to the recently created
nspawn
directory
Bridged Network with a Wireless Connnection in Host using NAT:
- For a wireless connection in host, we cannot add the interface directly to the bridge, instead we need to use
NAT
usingiptables
in our host. - Follow the first step in the
Bridged Network with a Wired Connection in host
to create a bridge. - Since, I want to provide a static ip address to my bridge interface, I did the below steps.
- If you have a properly configured dhcp service, you can make use of it.
$ sudo ip addr add 192.168.30.1/24 brd + dev [your brigde name]
# verify using the next command
$ ip a
...........Many other network interfaces................
9: virxen: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether f6:33:3c:bd:a8:40 brd ff:ff:ff:ff:ff:ff
inet 192.168.30.1/24 brd 192.168.30.255 scope global virxen
valid_lft forever preferred_lft forever
- For adding the NAT rules
$ sudo iptables -t nat -A POSTROUTING -s 192.168.30.0/24 ! -d 192.168.30.0/24 \
> -o [Your Internet Connected Wireles Interface Name] -j MASQUERADE
# Note: If you worry about security a lot, tweak the rule as per your requirement, as it a very generous rule
# verify using the next command
$ sudo iptables -L -t nat
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 192.168.30.0/24 !192.168.30.0/24
- Also, we need to tell our iptables to accept packets from our bridge to our wireless network interface. This is essential for our dns resolvers to work, otherwise dns resolution will fail
$ sudo iptables -A FORWARD -i virxen -o [your wireless interface name] -j ACCEPT
# verify using the next command
$ sudo iptables -L --verbose --line-numbers FORWARD
Chain FORWARD (policy DROP 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
.........................MANY OTHER NETWORK INTERFACES.................................
10 27 1608 ACCEPT all -- virxen [Your wirelsess interface name] anywhere anywhere
Note:
- If you’re not comfortable with messing around networking, please skip the above steps and use a minimal
nspawn
file - I’ll explain later in the series and it will default to host network.
- It will take some efforts to get a hang of configuring things correctly with networking.
These network rules that we have configured using iptables, will not persist across reboots, so you would need to find your way to keep them safely across reboots. Let’s cover the iptables rules persistence in the next section.