前言
市面上常见的容器化方案,像 Docker、Podman 或者 LXC/LXD,都是被广泛使用的技术。然而起步更早的 systemd-nspawn 却鲜有人关注。其实经过多年的发展,systemd-nspawn 也和其他方案一样使用了趋同的 Port Mapping、Mount Bind、Environment 等配置选项。
但是 systemd-nspawn 和其他方案相比,并不是一个「一站式」的解决方案,一方面它做的事情更加少,几乎只有「负责创建容器」这个功能,而网络、文件系统等功能比较依赖用户自己配置,另一方面也缺乏标准化的镜像打包和镜像仓库,几乎只能依赖用户自己创建 rootfs。因此对比起来,systemd-nspawn 更加「难用」,当然从另一个角度来说也更加「灵活」。
至于为什么在容器生态发展到今天,我依然要使用 systemd-nspawn,其实对我这个强迫症来说有个难以忍受的点:
「不喜欢没有边界感、乱动 nftables 的容器方案。」
对比一下前面的容器方案:
| Docker | Podman | LXC/LXD | systemd-nspawn | |
|---|---|---|---|---|
| 默认路由方式 (debian 12) | iptables | nftables | nftables | nftables |
| 是否支持 nftables | 不完全 | 是 | 是 | 不干涉 nftables |
| 是否支持完全关闭 nftables | 不完全 | 不完全 | 是 | 是 |
对于 Docker,即使在 config.josn 中关闭了 nftables,docker daemon 仍然会自动添加一些默认规则。
对于 LXC,iptables 规则由 lxc-net 管理。通过 systemd edit lxc-net 可以修改其启动顺序(来防止 nftables 配置刷新导致其规则被覆盖),或直接 mask 该 service 来手动管理所有网络配置。然而,LXC 强制依赖了 dnsmasq —— 而我不想使用它(而是自己定义 DHCP 服务)。
当然,并不是说乱动 nftables 的方案不好。它能提供一个简单的开箱即用的体验。不过,对于需要进行更加细致的路由设计和过滤的我来说,手搓 nftables 的方案更加符合个人的胃口。
此外不得不提的是,systemd-nspawn 依然有一个很大的缺点,即要求容器内的 Linux 必须使用 systemd 作为 init。
安装
Debian 12(下面均以 Debian 12 为例)上需要安装 systemd-container 包:
apt install systemd-container
创建镜像
可以使用 debootstrap 来创建一个 Debian 系统的 rootfs:
debootstrap --include=systemd,udev bookworm debian-12 https://deb.debian.org/debian
如果你没有安装 debootstrap,可以使用 apt install debootstrap 来安装。下面是每个参数的解释:
--include=systemd,udev:指定要包含的包。systemd-nspawn 需要容器镜像中包含 systemd 和 udev 来正常工作。debian-12:输出的 rootfs 目录。https://deb.debian.org/debian:指定 Debian 的镜像源。如果在国内,可以使用国内的镜像源来加速下载。
此外,也可以使用 --exclude 参数来排除一些不必要的包。我个人使用的命令为:
debootstrap --exclude=nano \
--include=systemd,dbus,jq,git,git-lfs,zip,7zip,unzip,bash-completion,vim,sudo,curl,wget,tree,tmux,file,zstd,dnsutils,iftop,iotop,btop,whois,rclone,rsync,gnupg,sqlite3,locales-all,uuid-runtime \
bookworm debian-12 https://deb.debian.org/debian
注意:虽然个
--include和--exclude参数可以预先安装一些常用的包,但是会显著增加 rootfs 的体积。具体的说,从最小可启动的镜像大小(128MB 左右)增加到 350MB 左右。
压缩和 OverlayFS
对于存储空间不敏感的情况,可以直接跳过此节,将 debian-12 文件夹移动到 /var/lib/machines/ 下。需要注意的是,虚拟机名字不可以超过 64 个字符,也不能包含特殊字符(如 /、: 等)。
首先,将刚刚创建的镜像压缩为 squashfs。如果你没有安装 squashfs-tools,可以使用 apt install squashfs-tools 来安装。
mksquashfs debian-12 debian-12.squashfs -comp zstd
可以使用 -comp 参数来指定压缩算法(gzip、lz4、xz 等),-Xcompression-level 参数来指定压缩级别(1-9)。使用 zstd 时,默认的压缩级别时 15 并已经启用多线程,对于大多数用户来说不需要修改。
完成 squashfs 的创建后,就不再需要 debian-12 文件夹了,可以将其删除。
接下来需要创建 overlayfs。本文使用 /srv/containers 作为容器的存储目录,具体的说,是这样的文件结构:
/srv/containers/images/.squashfs - 系统的 squashfs 镜像
/srv/containers/images/ - 系统 squashfs 镜像的只读挂载目录
/srv/containers/overlay//diff - 实际存储 systemd-nspawn 容器数据的目录
/srv/containers/overlay//work - OverlayFS 需要的缓存目录
其中,「实际存储 systemd-nspawn 容器数据的目录」是指容器内系统相比镜像文件的增量数据;这样就可以在容器之间共享基础的镜像,来节省存储空间。上述的目录皆可自定义,只需要在下文中将相关的目录替换为你自己的目录即可。
现在,mount squashfs 镜像到 /srv/containers/images/debian-12 目录:
mkdir -p /srv/containers/images/debian-12
mount -t squashfs -o loop debian-12.squashfs /srv/containers/images/debian-12
默认挂在的 squashfs 是只读的,这是符合预期的行为。
然后,创建 overlayfs:
mkdir /srv/containers/overlay/vm001/{diff,work}
mount -t overlay overlay -o metacopy=on,lowerdir=/srv/containers/images/debian-12,upperdir=/srv/containers/overlay/vm001/diff,workdir=/srv/containers/overlay/vm001/work /var/lib/machines/vm001
其中:
metacopy=on:开启元数据存储。由于 systemd-nspawn 会将容器内的 root 映射为宿主机上的普通用户(来提高安全性),而 overlayfs 默认会将权限不同的文件存储为两份数据。开启此选项后,overlayfs 只会在实际文件变化时,才会将数据存储为两份;如果只是权限文件的 uid/gid 变化,那么只会存储一份。lowerdir:squashfs 的挂载目录,也就是基础镜像挂载出来的目录。upperdir:overlayfs 的增量数据目录。workdir:overlayfs 的工作目录。/var/lib/machines/vm001:实际的容器根目录,必须是/var/lib/machines/的形式,才能被 systemd-nspawn 识别。
如果你需要固化上述操作,你需要编辑 /etc/fstab 文件,添加以下内容:
/srv/containers/images/debian-12 /srv/containers/images/debian-12 squashfs loop 0 0
/srv/containers/overlay/vm001 /var/lib/machines/vm001 overlay metacopy=on,lowerdir=/srv/containers/images/debian-12,upperdir=/srv/containers/overlay/vm001/diff,workdir=/srv/containers/overlay/vm001/work 0 0
如果你需要添加多个容器,也需要多次在 /etc/fstab 中添加对应的行。
配置密码
systemd-nspawn 容器默认是没有 root 密码的,因此需要手动设置。使用下面的命令进入容器:
systemd-nspawn -D /var/lib/machines/ -U
进入容器后,使用 passwd 命令设置 root 密码:
passwd
配置网络
网络的配置有很多选项,包括桥接、veth、MACVLAN 等等。
如果你的 VPS 提供商没有开启 MAC 过滤且 TOS 允许,可以使用 MACVLAN 方案。对于一般需求,请使用桥接方案。
当然,我个人是有一些特殊需求的,由于我的 VPS 有 IX Port,而受限于相关规定:
- 必须使用指定的 MAC 地址
- 禁止在 IX 网卡上做桥接
- BGP 会话必须使用指定的 IP 地址发起
因此,我的需求需要使用 IPVLAN 模式。下面我会介绍常用的几种配置方式。
使用 MACVLAN 模式配置
使用下面的命令启动 systemd-nspawn 容器:
systemd-nspawn -D /var/lib/machines/ -U --private-network --network-macvlan=eth0
其中 eth0 是 MACVLAN 父接口的名称。
使用 IPVLAN 模式配置
使用下面的命令启动 systemd-nspawn 容器:
systemd-nspawn -D /var/lib/machines/ -U --private-network --network-ipvlan=eth0
其中 eth0 是 MACVLAN 父接口的名称。容器内的 IP 需要和宿主机在同一个子网内,且不能和同局域网的其他 IP 冲突。
使用桥接模式配置
桥接模式和 VMWare/VirtualBox 的 Host-Only 模式类似,允许容器和宿主机在同一个网络中通信。
- 如果不配置 NAT 规则,那么容器只能和宿主机通信,无法访问外网。
- 如果配置 NAT 规则,那么和 VMWare/VirtualBox 的 NAT 模式以及 Docker 的 bridge 模式类似,容器通过宿主机的外网 IP 访问外网。
首先需要创建一个桥。对于 debian,首先需要安装 bridge-utils 包:
apt install bridge-utils
血的教训:如果忘记安装
bridge-utils,会导致重启networking.service时失败,直接把整个 VPS 的网络炸掉。
然后编辑 /etc/network/interfaces 文件,添加以下内容:
auto br0
iface br0 inet manual
bridge_ports eth0
bridge_stp off
bridge_fd 0
bridge_maxwait 0
up ip link set dev $IFACE up
down ip link set dev $IFACE down
如果你需要在桥上添加宿主机侧的 IP 地址,可以在 iface br0 inet manual 行下添加:
post-up ip addr add 10.0.0.1/24 dev $IFACE
你需要把 10.0.0.1/24 替换为你想要的 IP 地址和子网掩码。多个 IP 地址可以添加多行。虽然你也可以用 address 10.0.0.1/24 来添加 IP 地址,但是经过实测存在两个问题:
address只能添加一个 IP 地址。- 添加 IPv6 的时候会进行 DAD IP 冲突检测,而如果此时没有虚拟机在运行,会导致卡住 10 ~ 30 秒然后 DAD timeout。
因此,使用 post-up 的方式添加 IP 地址是更好的选择。
修改完 /etc/network/interfaces 文件后,重启 networking.service:
systemctl restart networking.service
然后,可以使用下面的命令启动 systemd-nspawn 容器:
systemd-nspawn -D /var/lib/machines/ -U --private-network --network-bridge=br0
使用 veth 模式配置
Veth 是更加底层的模式,你可以认为是一根虚拟网线,一端连着宿主机,一端连着容器,中间没有任何交换机、桥和路由器。
使用 Veth 模式你要准备两个 IP 地址:
- 一个宿主机的 IP 地址。需要注意的是,每创建一个新的容器,都会创建一个新的宿主机上的 veth 接口,因此需要为每个容器分配两个 IP 地址,一个在宿主机上,一个在容器内。下面的例子中使用
10.0.0.1/30。 - 一个容器内的 IP 地址。需要注意的是,容器内的 IP 地址必须和宿主机在同一个子网内。下面的例子中使用
10.0.0.2/30。
你需要编辑 /etc/network/interfaces 文件,添加以下内容:
请打开两个 ssh 窗口。在第一个 ssh 窗口中,执行下面的命令:
systemd-nspawn -D /var/lib/machines/ -U --private-network
在第二个 ssh 窗口中,执行下面的命令:
ip link show
查找以 ve- 开头的网卡。
- 如果你的容器名称小于 12 个字符,那么网卡名称为
ve-。 - 如果你的容器名称大于等于 12 个字符,那么网卡名称为
ve-。
接下来的配置中将以 ve-vm001 为例,你需要将其替换为你实际的网卡名称。
编辑 /etc/network/interfaces 文件,添加以下内容:
allow-hotplug ve-vm001
iface ve-vm001 inet static
address 10.0.0.1/30
然后,重启 networking.service:
systemctl restart networking.service
需要注意的是,这里使用 allow-hotplug 而不是 auto,因为 veth 是 systemd 动态添加的,如果使用 auto 会造成系统重启时网络初始化失败从而断网,以及不会自动查找动态添加的 veth 接口。
在容器内配置 IP 地址
容器内配置 IP 地址的方式和正常的 Debian 一样,编辑 /etc/network/interfaces 文件。
对于 DHCP,添加以下内容:
auto host0
iface host0 inet dhcp
对于静态 IP,添加以下内容:
auto host0
iface host0 inet static
address IP地址/CIDR
gateway 网关地址
dns-nameservers DNS服务器地址
对于静态 IP,你还需要修改 /etc/resolv.conf 文件,添加 DNS 服务器地址:
echo "nameserver 1.1.1.1" > /etc/resolv.conf
需要注意的是,必须使用 auto 而不是 allow-hotplug,因为 host* 接口不会触发 udev 事件。
下面是关于容器内 IP 地址配置的注意事项:
- 如果使用 MACVLAN 模式,且你的 LAN 内有 DHCP 服务器,可以直接使用 DHCP 来获取 IP 地址。
- 如果使用 MACVLAN 模式,且 LAN 内无 DHCP 服务器,或 DHCP 服务期有 MAC 白名单(典型的场景是 VPS 上把 IPv6 分配给容器),那么需要手动配置静态 IP 地址。该地址必须是你的 LAN 内的地址,或者是 VPS 提供商分配给你的 IPv6 地址段内的地址。
- 如果使用 IPVLAN 模式,只能使用静态 IP 地址,因为 IPVLAN 模式会和宿主机使用相同的 MAC 地址。
- 如果使用桥接模式,且你将宿主机的主网卡桥接到
br0,且 你的 LAN 内有 DHCP 服务器,可以直接使用 DHCP 来获取 IP 地址。 - 如果使用桥接模式,虽然没有桥接到主网卡,但是你在任意一个容器或者宿主机中运行了 DHCP 服务,那么可以直接使用 DHCP 来获取 IP 地址。
- 桥接模式的其他情况,需要手动配置静态 IP 地址。该地址必须和你宿主机上的
br0接口在同一个子网内。 - 如果使用 veth 模式,必须手动配置静态 IP 地址。该地址必须和宿主机上的
ve-接口在同一个子网内。
在容器中运行 Docker,以及固化配置
需要确保 /etc/systemd/nspawn 文件夹存在。
mkdir -p /etc/systemd/nspawn
然后,创建 /etc/systemd/nspawn/.nspawn 文件。需要注意的是,etc/systemd/nspawn/.nspawn 的容器名必须和 /var/lib/machines/ 中的目录名一致。
在该文件内添加以下内容:
[Network]
Private=yes
# 如果使用 MACVLAN 模式,需要添加下面这行。将 eth0 替换为 MACVLAN 父接口的名称。
MACVLAN=eth0
# 如果使用 IPVLAN 模式,需要添加下面这行。将 eth0 替换为 IPVLAN 父接口的名称。
IPVLAN=eth0
# 如果使用桥接模式,需要添加下面这行。将 br0 替换为宿主机上桥接口的名称。
Bridge=br0
# 如果使用 veth 模式,需要添加下面这行。将 ve-vm001 替换为容器内的 veth 接口名称。不支持自定义 veth 接口名称。
VirtualEthernet=yes
如果需要在容器中运行 Docker,可以在 /etc/systemd/nspawn/.nspawn 文件中添加以下内容:
[Exec]
SystemCallFilter=@keyring keyctl
此外还可以在 [Exec] 部分添加其他的配置选项,比如:
LimitCPU=100%
LimitRSS=1G
LimitNPROC=1000
来限制 CPU、内存和进程数。
容器启停相关命令以及设置开机自启
Systemd,启动!
和 systemctl 类似,可以像管理服务一样来管理 systemd-nspawn 容器。
- 查看运行中的容器:
machinectl(需要注意的是,machinectl只会显示正在运行的容器。停止的容器即使存在于/var/lib/machines/目录下,也不会显示在machinectl中。) - 启动容器:
machinectl start - 停止容器:
machinectl stop - 查看容器状态:
machinectl status - 容器开机自启:
machinectl enable - 取消容器开机自启:
machinectl disable
怎么样,是不是和 systemctl 管理服务一模一样?
怎么登录 systemd-nspawn 容器
最简单的办法是使用 machinectl login 命令。但是这里有个坑:
如果你用的是 SSH 登录,那么除非你停止这个容器,否则无法回到宿主机的 shell 中。
因此,推荐的方法是在容器里安装 ssh 服务。安装方式和正常的 Debian 系统一样,此处就不再赘述。
此外,在容器中也可以用 reboot 或 poweroff 命令来重启或停止容器。
容器 NAT 相关的配置
如果使用 nftabels 作为防火墙,配置容器 NAT 上网很简单。编辑 /etc/nftables.conf 文件,添加以下内容:
table inet nat {
chain postrouting {
type nat hook postrouting priority srcnat;
policy accept;
ip saddr 10.0.0.0/24 counter masquerade;
}
}
其中,把 10.0.0.0/24 替换为你容器内的 IP 地址段。
然后,重启 nftables 服务:
systemctl restart nftables.service
你可能需要设置 nftables 服务开机自启:
systemctl enable --now nftables.service
如果你有其他干扰 nftables/iptables 的服务,那么你可能还要调整他们的开机启动顺序。
如果使用 MACVLAN/IPVLAN 模式,或者把外网接口接到了桥上,那么你是大概率不需要 NAT 的,因为 NAT 由 LAN 的路由器完成了。

沙发
赞一个!
一直受不了docker在我的iptables里拉屎… 这个systemd-nspawn是个好东西