systemd-nspawn
systemd-nspawn 类似于 chroot 命令,但它是加强版的 chroot。
systemd-nspawn 可用于在轻量级命名空间容器中运行命令或操作系统。它比 chroot 更强大,因为它完全虚拟化了文件系统层级,以及进程树、各种 IPC 子系统以及主机名和域名。
systemd-nspawn 将容器内对各种内核接口(如 /sys、/proc/sys 或 /sys/fs/selinux)的访问限制为只读。网络接口和系统时钟不能从容器内部更改。不能创建设备节点。不能从容器内部重启宿主机,也不能加载内核模块。
相比 LXC 或 Libvirt,systemd-nspawn 是一个配置更简单的工具。
安装
systemd-nspawn 是 systemd 的一部分并包含在其中。
示例
创建并引导一个最小化 Arch Linux 容器
创建一个目录来存放容器,在本例中我们将使用 ~/MyContainer。
使用来自 arch-install-scripts 包的 pacstrap 在容器中安装一个基础 Arch 系统。我们至少需要安装 base 包。
# pacstrap -K -c ~/MyContainer base [additional packages/groups]
安装完成后,进入容器,并设置 root 密码:
# systemd-nspawn -D ~/MyContainer # passwd # logout
machinectl shell root@MyContainer 直接在已启动的容器中获取 root shell,而无需登录。参见 #machinectl。最后,引导进入容器:
# systemd-nspawn -b -D ~/MyContainer
-b 选项将引导容器(即运行 systemd 作为 PID=1),而不是仅仅运行 shell;而 -D 指定成为容器根目录的目录。
容器启动后,使用您的密码以 "root" 身份登录。
通过在容器内运行 poweroff 可以关闭容器电源。在宿主机上,可以通过 machinectl 工具控制容器。
Ctrl 并快速按 ] 三次。创建 Debian 或 Ubuntu 环境
安装 debootstrap,并根据所需的发行版安装 debian-archive-keyring 或 ubuntu-keyring(或两者都安装)。
然后按以下结构调用 deboostrap:
# debootstrap [OPTIONS...] SUITE TARGET [MIRROR]
- SUITE(必填)是 scripts 目录中特定发行版版本的代号或别名。
- 对于 Debian,有效的 suite 名称是稳定版别名
stable、testing和unstable,或版本名称如bookworm和sid:列表参见 [1]。 - 对于 Ubuntu,应仅使用版本代号(如
jammy和noble),而不应使用版本号:参见 [2] 和 [3] 获取代号与版本号对照表。 - scripts 目录中还存在其他基于 Debian 的发行版引用,如 Devuan、eLxr、Kali Linux、Pardus、PureOS、Trisquel 和 Tanglu(2017年后停更)。调用这些通常需要获取其特定的密钥环并传递给
--keyring选项,或使用--no-check-sig禁用 Release 文件的 OpenPGP 签名检查。
- 对于 Debian,有效的 suite 名称是稳定版别名
- TARGET(必填)是包含 debootstrap 系统的目录;如果不存在则会创建。
- MIRROR(可选):下载软件包的存档 URL。对于目前的 Debian 版本,可以是任何 有效镜像,例如 CDN 支持的 https://deb.debian.org/debian(默认);对于 Ubuntu,可以是 [4] 中的任何镜像,例如参考地址 https://archive.ubuntu.com/ubuntu(也是默认使用)。
- 注意: 已存档的 Debian 版本(截至 2025-01,指 Debian 10/Buster 之前的版本)使用特殊的 debian-archive 镜像 URL https://archive.debian.org/debian/。对于 Ubuntu,脚本使用 https://old-releases.ubuntu.com/ubuntu/。然而请注意,对于上述密钥环包中未包含的版本,会出现“未知签名密钥”问题,例如 Debian 9 ("Stretch") 以前的任何版本[5],这使得后者成为在不手动获取不同密钥环或不禁用签名检查的情况下可安装的最旧版本。
debootstrap 无法解决虚拟包的依赖关系[6],因此默认不安装 systemd 推荐的 dbus 和 libpam-systemd 依赖项[7][8]。因此,某些与 systemd/dbus 相关的功能(例如 localectl)以及使用 #machinectl 管理容器的功能无法开箱即用。
为了在基于 systemd 的系统中获得完整功能,请在 debootstrap 调用时添加 --include=dbus,libpam-systemd,或者在容器中安装这些软件包:
# debootstrap --include=dbus,libpam-systemd,libnss-systemd stable /path/to/machine
-b, --boot 选项)未使用 systemd 作为其 init 的容器时,可能会遇到其他困难。Debian 自 Debian 8 ("jessie")[9] 起,Ubuntu 自 15.04 ("vivid")[10] 起将 systemd 作为默认 init 系统。无论 init 系统如何,获取 shell 应该都能正常工作。与 Arch 类似,Debian 和 Ubuntu 在没有密码的情况下不允许登录。要设置 root 密码,请运行不带 -b 选项的 systemd-nspawn:
# systemd-nspawn -D /path/to/machine # passwd # logout
使用 Podman 或 Docker 创建 RHEL 衍生环境
这些指令应适用于任何使用 dnf 的发行版。您将需要 Podman 或 Docker。
# mkdir -p /var/lib/machines/my-machine # podman pull centos:stream9 # podman run --rm -it -v /var/lib/machines/my-machine:/machine centos:stream9
在该容器内部,您可以为您的机器构建根环境:
bash-5.1# dnf update -y
bash-5.1# dnf --repo=baseos \
--releasever=9 \
--best \
--installroot=/machine \
install \
systemd-udev \
hostname \
yum \
dnf \
centos-gpg-keys \
centos-stream-release \
rootfiles \
shadow-utils \
util-linux
bash-5.1# sed -i 's/^root:[^:]*:/root::/' /machine/etc/shadow # remove root password for initial login
bash-5.1# exit
之后,您应该能够引导机器:
# systemd-nspawn --machine=my-machine --boot
创建 Fedora 或 AlmaLinux 环境
安装 dnf,并编辑 /etc/dnf/dnf.conf 文件以添加所需的 Fedora 存储库。
/etc/dnf/dnf.conf
[fedora] name=Fedora $releasever - $basearch metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch gpgkey=https://fedoraproject.org/fedora.gpg [updates] name=Fedora $releasever - $basearch - Updates metalink=https://mirrors.fedoraproject.org/metalink?repo=updates-released-f$releasever&arch=$basearch gpgkey=https://fedoraproject.org/fedora.gpg
fedora.gpg 文件包含最新 Fedora 版本的 GPG 密钥 https://fedoraproject.org/security。要设置一个最小的 Fedora 42 容器:
# mkdir /var/lib/machines/container-name # dnf5 --releasever=42 --best --use-host-config --setopt=install_weak_deps=False --repo=fedora --repo=updates --installroot=/var/lib/machines/container-name install dhcp-client dnf fedora-release glibc glibc-langpack-en iputils less ncurses passwd systemd systemd-networkd systemd-resolved util-linux vim-default-editor
如果您正在使用 btrfs 文件系统,请创建子卷而不是目录。
像 AlmaLinux 这样的企业级 Linux 衍生版默认启用了三个存储库:包含所有安装基础的核心集的 BaseOS、包含额外应用程序和语言包等的 AppStream,以及包含 RHEL 中未包含的包的 Extras。因此,对于最小化容器,我们只需将 BaseOS 存储库添加到 /etc/dnf/dnf.conf 即可:
/etc/dnf/dnf.conf
[baseos] name=AlmaLinux $releasever - BaseOS mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/baseos gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
要创建一个 AlmaLinux 9 最小容器:
# dnf --repo=baseos --releasever=9 --best --installroot=/var/lib/machines/container-name --setopt=install_weak_deps=False install almalinux-release dhcp-client dnf glibc-langpack-en iproute iputils less passwd systemd vim-minimal
这将安装 AlmaLinux 9 的最新小版本。您可以选择安装特定的小版本,但需要手动将 gpgpkey 条目更改为指向 RPM-GPG-KEY-AlmaLinux-9。
与 Arch 一样,Fedora 或 AlmaLinux 在没有密码的情况下不允许以 root 身份登录。要设置 root 密码,请运行不带 -b 选项的 systemd-nspawn:
# systemd-nspawn -D /var/lib/machines/container-name passwd
构建和测试软件包
示例用法请参阅 为其他发行版创建软件包。
管理
位于 /var/lib/machines/ 中的容器可以通过 machinectl 命令进行控制,该命令在内部控制 systemd-nspawn@.service 单元的实例。/var/lib/machines/ 中的子目录对应于容器名称,例如 /var/lib/machines/container-name/。
/var/lib/machines/ 中,可以使用符号链接。详见 machinectl(1) § 文件和目录。默认 systemd-nspawn 选项
请注意,通过 machinectl 或 systemd-nspawn@.service 启动的容器与通过 systemd-nspawn 命令手动启动的容器使用不同的默认选项。该服务使用的额外选项包括:
-b/--boot– 托管容器会自动搜索 init 程序并将其作为 PID 1 调用。--network-veth暗含了--private-network– 托管容器获得一个虚拟网络接口,并与宿主机网络断开。详见 #网络。-U– 如果内核支持,托管容器默认使用 user_namespaces(7) 功能。影响请见 #非特权容器。--link-journal=try-guest
可以在逐个容器的配置文件中覆盖此行为。详见 #配置。
machinectl
可以通过 machinectl subcommand container-name 命令管理容器。例如,要启动容器:
$ machinectl start container-name
machinectl start container_name 将导致错误 Invalid machine name container_name。详见 [12] 和 [13]。类似地,还有 poweroff、reboot、status 和 show 等子命令。详见 machinectl(1) § 机器命令。
poweroff 和 reboot 命令执行。其他常用命令包括:
machinectl list– 显示当前正在运行的容器列表machinectl login container-name– 在容器中开启一个交互式登录会话machinectl shell [username@]container-name– 在容器中开启一个交互式 shell 会话(这将直接调用用户进程,而不经过容器内的登录过程)machinectl enable container-name和machinectl disable container-name– 启用或禁用容器开机自启,详情请见 #启用容器开机自启
machinectl 还有管理容器(或虚拟机)镜像和镜像传输的子命令。详情请见 machinectl(1) § 镜像命令 和 machinectl(1) § 镜像传输命令。截至 2023Q1,machinectl(1) § 示例 的前 3 个例子演示了镜像传输命令。machinectl(1) § 文件和目录 讨论了在哪里可以找到合适的镜像。
systemd 工具链
大部分 core systemd 工具链都已更新以支持容器。支持的工具通常提供 -M, --machine= 选项,该选项接受容器名称作为参数。
示例
查看特定机器的 journal 日志:
# journalctl -M container-name
显示控制组内容:
$ systemd-cgls -M container-name
查看容器启动时间:
$ systemd-analyze -M container-name
资源使用概览:
$ systemd-cgtop
配置
逐个容器设置
要指定逐个容器的设置而非全局覆盖,可以使用 .nspawn 文件。详情请见 systemd.nspawn(5)。
启用容器开机自启
如果经常使用某个容器,您可能希望在引导时启动它。
首先确保 machines.target 已 启用 (enabled)。
可被 machinectl 发现的容器可以启用或禁用:
$ machinectl enable container-name
- 这会产生启用
systemd-nspawn@container-name.servicesystemd 单元的效果。 - 如 #默认 systemd-nspawn 选项 中所述,由 machinectl 启动的容器会获得一个虚拟以太网接口。要禁用私有网络,请参阅 #主机网络。
资源控制
您可以利用控制组来通过 systemctl set-property 对容器实施限制和资源管理,详见 systemd.resource-control(5)。例如,您可能想要限制内存量或 CPU 使用。要将容器的内存消耗限制在 2 GiB:
# systemctl set-property systemd-nspawn@container-name.service MemoryMax=2G
或将 CPU 时间使用量限制在约相当于 2 个核心:
# systemctl set-property systemd-nspawn@container-name.service CPUQuota=200%
这将在 /etc/systemd/system.control/systemd-nspawn@container-name.service.d/ 中创建永久文件。
根据文档,MemoryHigh 是控制内存消耗的首选方法,但它不会像 MemoryMax 那样进行硬性限制。您可以同时使用这两个选项,将 MemoryMax 作为最后一道防线。另外请考虑,您无法限制容器可以看到的 CPU 核心数,但可以通过限制容器最大能获得的总 CPU 时间比例来实现类似的效果。
--runtime 选项。您可以使用 systemd-cgtop 检查结果。网络
systemd-nspawn 容器可以使用 主机网络 或 私有网络
- 在主机网络模式下,容器可以完全访问宿主机网络。这意味着容器能够访问宿主机上的所有网络服务,并且来自容器的数据包在外部网络看来就像来自宿主机一样(即共享同一个 IP 地址)。
- 在私有网络模式下,容器与宿主机的网络断开。这使得容器无法使用所有网络接口,但回环设备和明确分配给容器的接口除外。有多种方式为容器设置网络接口:
- 可以将现有的接口分配给容器(例如,如果您有多个以太网设备):参见 #使用现有接口。
- 可以创建与现有接口关联的虚拟网络接口(即 VLAN 接口)并分配给容器:参见 #使用 "macvlan" 或 "ipvlan" 接口。
- 可以创建宿主机和容器之间的虚拟以太网链路:参见 #使用虚拟以太网链路。
- 在后一种情况下,容器的网络是完全隔离的(与外部网络及其他容器隔离),需要管理员配置宿主机和容器之间的网络。这通常涉及在多个接口之间 使用 NAT 网络 或 使用网络桥接 来连接多个(物理或虚拟)接口。
主机网络模式适用于不需要运行任何配置接口的网络软件的应用容器。当您从 shell 运行 systemd-nspawn 时,主机网络是默认模式。
另一方面,私有网络模式适用于应与宿主机系统隔离的系统容器。创建虚拟以太网链路是一个非常灵活的工具,允许创建复杂的虚拟网络。这是由 machinectl 或 systemd-nspawn@.service 启动的容器的默认模式。
以下小节描述了常见方案。有关可用 systemd-nspawn 选项的详情,请参阅 systemd-nspawn(1) § 网络选项。
主机网络
要禁用私有网络和禁止创建由 machinectl 启动的容器所使用的虚拟以太网链路,请添加包含以下选项的 .nspawn 文件:
/etc/systemd/nspawn/container-name.nspawn
[Network] VirtualEthernet=no
这将覆盖 systemd-nspawn@.service 中使用的 -n/--network-veth 选项,新启动的容器将使用主机网络模式。
私有网络
使用虚拟以太网链路
如果使用 -n/--network-veth 选项启动容器,systemd-nspawn 会在宿主机和容器之间创建虚拟以太网链路。链路的主机端将作为名为 ve-container-name 的网络接口可用。链路的容器端将被命名为 host0。请注意,此选项暗示了 --private-network。
- 如果容器名称太长,接口名称将被缩短(例如用
ve-long-conKQGh代替ve-long-container-name)以适应 15 个字符的限制。全名将设置为接口的altname属性(参见 ip-link(8)),仍可用于引用该接口。 - 使用
ip link查看接口时,接口名称将显示带有后缀,如ve-container-name@if2和host0@if9。@ifN实际上不是接口名称的一部分;相反,ip link附加此信息以指示虚拟以太网电缆另一端连接到哪个“插槽”。
- 例如,显示为
ve-foo@if2的主机虚拟以太网接口已连接到容器foo,而在容器内部连接到第二个网络接口——即在容器内运行ip link时显示索引为 2 的那个。类似地,容器中名为host0@if9的接口连接到宿主机上的第 9 个网络接口。
启动容器时,必须为两个接口(在宿主机和容器中)分配 IP 地址。如果您在宿主机和容器中都使用 systemd-networkd,这将开箱即用:
- 宿主机上的
/usr/lib/systemd/network/80-container-ve.network文件匹配ve-container-name接口并启动 DHCP 服务器,为宿主机接口和容器分配 IP 地址; - 容器中的
/usr/lib/systemd/network/80-container-host0.network文件匹配host0接口并启动 DHCP 客户端,从宿主机接收 IP 地址。
systemd-networkd 即可提供所述容器网络自动配置,且不会干扰现有网络设置,因为现有接口在另行配置前仍由 systemd-networkd 保持“非托管”状态。因此,它与托管网络设置(例如使用 NetworkManager)完美兼容,启动 systemd-networkd 确实是在不干扰宿主机任何内容的情况下获得容器内互联网连接(以及从宿主机访问它们)所需的全部操作。如果您不使用 systemd-networkd,可以配置静态 IP 地址,或在主机接口上启动 DHCP 服务器并在容器中启动 DHCP 客户端。详情请见 网络配置。
使用 NAT 网络
要让容器访问外部网络,您可以按照 互联网共享#启用 NAT 所述配置 NAT。如果您使用 systemd-networkd,这会通过 /usr/lib/systemd/network/80-container-ve.network 中的 IPMasquerade=both 选项(部分)自动完成。但是,这只会发出一条 iptables(或 nftables)规则,例如:
-t nat -A POSTROUTING -s 192.168.163.192/28 -j MASQUERADE
filter 表必须手动配置,如 互联网共享#启用 NAT 所示。您可以使用通配符来匹配所有以 ve- 开头的接口:
# iptables -A FORWARD -i ve-+ -o internet0 -j ACCEPT
此外,您需要在 ve-+ 接口上开启 UDP 端口 67,以便允许与 DHCP 服务器(由 systemd-networkd 运行)的入站连接:
# iptables -A INPUT -i ve-+ -p udp -m udp --dport 67 -j ACCEPT
使用网络桥接
如果您在宿主机系统中配置了 网络桥接,可以为容器创建虚拟以太网链路并将其主机端添加到该网络桥接中。这通过 --network-bridge=bridge-name 选项实现。请注意,--network-bridge 暗含 --network-veth,即虚拟以太网链路会自动创建。不过,链路的主机端将使用 vb- 前缀而不是 ve-,因此用于启动 DHCP 服务器和 IP 伪装的 systemd-networkd 选项将不适用。
网桥管理留给管理员处理。例如,网桥可以将虚拟接口与物理接口连接,也可以仅连接多个容器的虚拟接口。使用 systemd-networkd 的示例配置请见 systemd-networkd#使用 DHCP 的网络桥接 和 systemd-networkd#使用静态 IP 地址的网络桥接。
还有一个 --network-zone=zone-name 选项,它类似于 --network-bridge,但网络桥接由 systemd-nspawn 和 systemd-networkd 自动管理。当启动第一个配置了 --network-zone=zone-name 的容器时,会自动创建名为 vz-zone-name 的网桥接口;当最后一个配置该选项的容器退出时,该接口会自动删除。因此,此选项可以轻松地将多个相关的容器放在同一个通用虚拟网络中。注意 vz-* 接口由 systemd-networkd 使用 /usr/lib/systemd/network/80-container-vz.network 文件中的选项管理,方式与 ve-* 接口相同。
使用 "macvlan" 或 "ipvlan" 接口
除了创建虚拟以太网链路(其主机端可能添加到网桥,也可能不添加),您还可以在现有物理接口上创建虚拟接口(即 VLAN 接口)并将其添加到容器中。虚拟接口将与基础宿主机接口桥接,从而使容器暴露在外部网络中,这允许它从宿主机所在的同一 LAN 通过 DHCP 获取独立的 IP 地址。
systemd-nspawn 提供 2 个选项:
--network-macvlan=interface– 虚拟接口将具有与基础物理interface不同的 MAC 地址,并命名为mv-interface。--network-ipvlan=interface– 虚拟接口将具有与基础物理interface相同的 MAC 地址,并命名为iv-interface。
这两个选项都暗含 --private-network。
使用现有接口
如果宿主机系统有多个物理网络接口,您可以使用 --network-interface=interface 将 interface 分配给容器(并在启动容器期间使其在宿主机上不可用)。请注意 --network-interface 暗含了 --private-network。
端口映射
当启用私有网络时,可以使用 -p/--port 选项或通过 .nspawn 文件中的 Port 设置,将宿主机上的单个端口映射到容器上的端口。例如,将宿主机上的 TCP 端口 8000 映射到容器内的 TCP 端口 80:
/etc/systemd/nspawn/container-name.nspawn
[Network] Port=tcp:8000:80
这通过向 nat 表发布 iptables(或 nftables)规则来实现,但 filter 表中的 FORWARD 链需要按照 #使用虚拟以太网链路 中所示手动配置。此外,如果您遵循了 简易状态防火墙,请运行以下命令以允许到宿主机 wan_interface 的新连接在转发端口上建立:
# iptables -A FORWARD -i wan_interface -o ve-+ -p tcp --syn --dport 8000 -m conntrack --ctstate NEW -j ACCEPT
loopback 接口。因此,在上述示例中,localhost:8000 连接到宿主机而不是容器。只有到其他接口的连接才会进行端口映射。详情请见 [18]。域名解析
容器中的 域名解析 可以按照与宿主机系统相同的方式进行配置。此外,systemd-nspawn 还提供了管理容器内 /etc/resolv.conf 文件的选项:
--resolv-conf可在命令行使用ResolvConf=可在 .nspawn 文件中使用
这些相应的选项有许多可能的值,在 systemd-nspawn(1) § 集成选项 中有详细说明。默认值为 auto,这意味着:
- 如果启用了
--private-network,则保持容器中的/etc/resolv.conf不变。 - 否则,如果宿主机正在运行 systemd-resolved,则将其 stub
resolv.conf文件复制或绑定挂载到容器中。 - 否则,将
/etc/resolv.conf文件从宿主机复制或绑定挂载到容器中。
在最后两种情况下,如果容器根目录是可写的,则复制该文件;如果是只读的,则绑定挂载。
对于宿主机运行 systemd-resolved 的第二种情况,systemd-nspawn 期望它也在容器中运行,以便容器可以使用宿主机的 stub 符号链接文件 /etc/resolv.conf。如果没有,默认值 auto 将不再有效,您应该通过使用 replace-* 选项之一来替换符号链接。
技巧与提示
运行非 shell/init 命令
- [选项]
--as-pid2将 shell 或指定程序作为进程 ID (PID) 2 而非 PID 1 (init) [调用]。[...] 除非已修改为能正确地作为 PID 1 运行,否则建议使用此模式在容器中调用任意命令。换句话说:几乎所有命令都应该使用这个开关,除非该命令指的是 init 或 shell 的具体实现 [...] 此选项不得与--boot结合使用。
无特权容器
systemd-nspawn 支持非特权容器,尽管容器仍需要以 root 身份引导。
实现此目的最简单的方法是使用 -U 选项让 systemd-nspawn 自动选择未使用的 UID/GID 范围:
# systemd-nspawn -bUD ~/MyContainer
如果内核支持用户命名空间,-U 选项等效于 --private-users=pick --private-users-ownership=auto。详情请见 systemd-nspawn(1) § 用户命名空间选项。
如果容器是使用 --private-users-ownership=chown 选项(或在 -U 强制要求该选项的文件系统上)以私有 UID/GID 范围启动的,则需要继续保持这种方式,以避免权限错误。或者,通过指定从 0 开始的 ID 范围,可以撤消 --private-users-ownership=chown 对容器文件系统的影响:
# systemd-nspawn -D ~/MyContainer --private-users=0 --private-users-ownership=chown
使用 X 环境
参见 Xhost 和 Change root#从 chroot 运行图形应用程序。
您需要在容器会话内设置 DISPLAY 环境变量,以便连接到外部 X 服务器。
X 在 /tmp 目录中存储一些所需的文件。为了让您的容器能够显示任何内容,它需要访问这些文件。为此,请在启动容器时附加 --bind-ro=/tmp/.X11-unix 选项。
/tmp/.X11-unix 内容 必须以只读方式绑定挂载,否则它们将从文件系统中消失。只读挂载标志不会阻止在套接字上使用 connect() 系统调用。如果您还绑定了 /run/user/1000,那么您可能需要明确地将 /run/user/1000/bus 绑定为只读,以保护 dbus 套接字不被删除。避免使用 xhost
xhost 仅向 X 服务器提供相当粗糙的访问权限。通过 $XAUTHORITY 文件可以实现更细粒度的访问控制。遗憾的是,仅使 $XAUTHORITY 文件在容器中可访问是行不通的:您的 $XAUTHORITY 文件特定于您的宿主机,但容器是不同的主机。可以使用以下改编自 stackoverflow 的技巧,让您的 X 服务器接受容器内运行的 X 应用程序提供的 $XAUTHORITY 文件:
$ XAUTH=/tmp/container_xauth $ xauth nextract - "$DISPLAY" | sed -e 's/^..../ffff/' | xauth -f "$XAUTH" nmerge - # systemd-nspawn -D myContainer --bind=/tmp/.X11-unix --bind="$XAUTH" -E DISPLAY="$DISPLAY" -E XAUTHORITY="$XAUTH" --as-pid2 /usr/bin/xeyes
上面的第二行将连接族设置为 "FamilyWild",值为 65535,这使得该条目匹配每个显示器。详情请见 Xsecurity(7)。
使用 X 嵌套/Xephyr
运行 X 应用程序并避免共享 X 桌面风险的另一种简单方法是使用 X 嵌套。这里的优势是完全避免了容器内应用程序与非容器应用程序之间的交互,并且能够运行不同的 桌面环境 或 窗口管理器。缺点是性能较低,且在使用 Xephyr 时缺乏硬件加速。
在容器外运行 Xephyr:
# Xephyr :1 -resizeable
然后使用以下选项启动容器:
--setenv=DISPLAY=:1 --bind-ro=/tmp/.X11-unix/X1
不需要其他绑定。
在某些情况下,您可能仍需要在容器中手动设置 DISPLAY=:1(主要是与 -b 配合使用时)。
运行 Firefox
# systemd-nspawn --setenv=DISPLAY=:0 \
--setenv=XAUTHORITY=~/.Xauthority \
--bind-ro=$HOME/.Xauthority:/root/.Xauthority \
--bind=/tmp/.X11-unix \
-D ~/containers/firefox \
--as-pid2 \
firefox
--user <用户名> 选项。或者,您可以引导容器并让例如 systemd-networkd 设置虚拟网络接口:
# systemd-nspawn --bind-ro=$HOME/.Xauthority:/root/.Xauthority \
--bind=/tmp/.X11-unix \
-D ~/containers/firefox \
--network-veth -b
容器引导后,像这样运行 Xorg 二进制文件:
# systemd-run -M firefox --setenv=DISPLAY=:0 firefox
3D 图形加速
要启用 3D 图形加速,可能需要通过在 .nspawn 文件中添加以下行,将 /dev/dri 绑定挂载到容器:
Bind=/dev/dri
上述技巧采用了来自 patrickskiba.com。这显著地解决了以下问题:
libGL error: MESA-LOADER: failed to retrieve device information libGL error: Version 4 or later of flush extension not found libGL error: failed to load driver: i915
您可以通过运行 glxinfo 或 glxgears 确认其已启用。
NVIDIA GPU
如果您无法在容器内安装与宿主机相同版本的 NVIDIA 驱动程序,可能还需要绑定驱动库文件。您可以在宿主机上运行 pacman -Ql nvidia-utils 以查看其包含的所有文件。您不需要复制所有内容。以下 systemd 覆盖文件将在通过 machinectl start container-name 运行容器时绑定所有必要文件。
/etc/systemd/system/systemd-nspawn@.service.d/nvidia-gpu.conf
[Service] ExecStart= ExecStart=systemd-nspawn --quiet --keep-unit --boot --link-journal=try-guest --machine=%i \ --bind=/dev/dri \ --bind=/dev/shm \ --bind=/dev/nvidia0 \ --bind=/dev/nvidiactl \ --bind=/dev/nvidia-modeset \ --bind=/usr/bin/nvidia-bug-report.sh:/usr/bin/nvidia-bug-report.sh \ --bind=/usr/bin/nvidia-cuda-mps-control:/usr/bin/nvidia-cuda-mps-control \ --bind=/usr/bin/nvidia-cuda-mps-server:/usr/bin/nvidia-cuda-mps-server \ --bind=/usr/bin/nvidia-debugdump:/usr/bin/nvidia-debugdump \ --bind=/usr/bin/nvidia-modprobe:/usr/bin/nvidia-modprobe \ --bind=/usr/bin/nvidia-ngx-updater:/usr/bin/nvidia-ngx-updater \ --bind=/usr/bin/nvidia-persistenced:/usr/bin/nvidia-persistenced \ --bind=/usr/bin/nvidia-powerd:/usr/bin/nvidia-powerd \ --bind=/usr/bin/nvidia-sleep.sh:/usr/bin/nvidia-sleep.sh \ --bind=/usr/bin/nvidia-smi:/usr/bin/nvidia-smi \ --bind=/usr/bin/nvidia-xconfig:/usr/bin/nvidia-xconfig \ --bind=/usr/lib/gbm/nvidia-drm_gbm.so:/usr/lib/x86_64-linux-gnu/gbm/nvidia-drm_gbm.so \ --bind=/usr/lib/libEGL_nvidia.so:/usr/lib/x86_64-linux-gnu/libEGL_nvidia.so \ --bind=/usr/lib/libGLESv1_CM_nvidia.so:/usr/lib/x86_64-linux-gnu/libGLESv1_CM_nvidia.so \ --bind=/usr/lib/libGLESv2_nvidia.so:/usr/lib/x86_64-linux-gnu/libGLESv2_nvidia.so \ --bind=/usr/lib/libGLX_nvidia.so:/usr/lib/x86_64-linux-gnu/libGLX_nvidia.so \ --bind=/usr/lib/libcuda.so:/usr/lib/x86_64-linux-gnu/libcuda.so \ --bind=/usr/lib/libnvcuvid.so:/usr/lib/x86_64-linux-gnu/libnvcuvid.so \ --bind=/usr/lib/libnvidia-allocator.so:/usr/lib/x86_64-linux-gnu/libnvidia-allocator.so \ --bind=/usr/lib/libnvidia-cfg.so:/usr/lib/x86_64-linux-gnu/libnvidia-cfg.so \ --bind=/usr/lib/libnvidia-egl-gbm.so:/usr/lib/x86_64-linux-gnu/libnvidia-egl-gbm.so \ --bind=/usr/lib/libnvidia-eglcore.so:/usr/lib/x86_64-linux-gnu/libnvidia-eglcore.so \ --bind=/usr/lib/libnvidia-encode.so:/usr/lib/x86_64-linux-gnu/libnvidia-encode.so \ --bind=/usr/lib/libnvidia-fbc.so:/usr/lib/x86_64-linux-gnu/libnvidia-fbc.so \ --bind=/usr/lib/libnvidia-glcore.so:/usr/lib/x86_64-linux-gnu/libnvidia-glcore.so \ --bind=/usr/lib/libnvidia-glsi.so:/usr/lib/x86_64-linux-gnu/libnvidia-glsi.so \ --bind=/usr/lib/libnvidia-glvkspirv.so:/usr/lib/x86_64-linux-gnu/libnvidia-glvkspirv.so \ --bind=/usr/lib/libnvidia-ml.so:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so \ --bind=/usr/lib/libnvidia-ngx.so:/usr/lib/x86_64-linux-gnu/libnvidia-ngx.so \ --bind=/usr/lib/libnvidia-opticalflow.so:/usr/lib/x86_64-linux-gnu/libnvidia-opticalflow.so \ --bind=/usr/lib/libnvidia-ptxjitcompiler.so:/usr/lib/x86_64-linux-gnu/libnvidia-ptxjitcompiler.so \ --bind=/usr/lib/libnvidia-rtcore.so:/usr/lib/x86_64-linux-gnu/libnvidia-rtcore.so \ --bind=/usr/lib/libnvidia-tls.so:/usr/lib/x86_64-linux-gnu/libnvidia-tls.so \ --bind=/usr/lib/libnvidia-vulkan-producer.so:/usr/lib/x86_64-linux-gnu/libnvidia-vulkan-producer.so \ --bind=/usr/lib/libnvoptix.so:/usr/lib/x86_64-linux-gnu/libnvoptix.so \ --bind=/usr/lib/modprobe.d/nvidia-utils.conf:/usr/lib/x86_64-linux-gnu/modprobe.d/nvidia-utils.conf \ --bind=/usr/lib/nvidia/wine/_nvngx.dll:/usr/lib/x86_64-linux-gnu/nvidia/wine/_nvngx.dll \ --bind=/usr/lib/nvidia/wine/nvngx.dll:/usr/lib/x86_64-linux-gnu/nvidia/wine/nvngx.dll \ --bind=/usr/lib/nvidia/xorg/libglxserver_nvidia.so:/usr/lib/x86_64-linux-gnu/nvidia/xorg/libglxserver_nvidia.so \ --bind=/usr/lib/vdpau/libvdpau_nvidia.so:/usr/lib/x86_64-linux-gnu/vdpau/libvdpau_nvidia.so \ --bind=/usr/lib/xorg/modules/drivers/nvidia_drv.so:/usr/lib/x86_64-linux-gnu/xorg/modules/drivers/nvidia_drv.so \ --bind=/usr/share/X11/xorg.conf.d/10-nvidia-drm-outputclass.conf:/usr/share/X11/xorg.conf.d/10-nvidia-drm-outputclass.conf \ --bind=/usr/share/dbus-1/system.d/nvidia-dbus.conf:/usr/share/dbus-1/system.d/nvidia-dbus.conf \ --bind=/usr/share/egl/egl_external_platform.d/15_nvidia_gbm.json:/usr/share/egl/egl_external_platform.d/15_nvidia_gbm.json \ --bind=/usr/share/glvnd/egl_vendor.d/10_nvidia.json:/usr/share/glvnd/egl_vendor.d/10_nvidia.json \ --bind=/usr/share/licenses/nvidia-utils/LICENSE:/usr/share/licenses/nvidia-utils/LICENSE \ --bind=/usr/share/vulkan/icd.d/nvidia_icd.json:/usr/share/vulkan/icd.d/nvidia_icd.json \ --bind=/usr/share/vulkan/implicit_layer.d/nvidia_layers.json:/usr/share/vulkan/implicit_layer.d/nvidia_layers.json \ DeviceAllow=/dev/dri rw DeviceAllow=/dev/shm rw DeviceAllow=/dev/nvidia0 rw DeviceAllow=/dev/nvidiactl rw DeviceAllow=/dev/nvidia-modeset rw
ldconfig 以更新库。访问宿主机文件系统
参见 systemd-nspawn(1) 中的 --bind 和 --bind-ro。
如果宿主机和容器都是 Arch Linux,那么可以共享例如 pacman 缓存:
# systemd-nspawn --bind=/var/cache/pacman/pkg
或者您可以使用以下文件指定单个容器的绑定:
/etc/systemd/nspawn/my-container.nspawn
[Files] Bind=/var/cache/pacman/pkg
参见 #逐个容器设置。
要将目录绑定到容器内的不同路径,请在路径后加冒号分隔。例如:
# systemd-nspawn --bind=/path/to/host_dir:/path/to/container_dir
如果是 #非特权容器,生成的挂载点将归 nobody 用户所有。这可以使用 idmap 挂载选项进行修改:
# systemd-nspawn --bind=/path/to/host_dir:/path/to/container_dir:idmap
在非 systemd 系统上运行
使用 Btrfs 子卷作为容器根目录
要使用 Btrfs 子卷 作为容器根目录的模板,请使用 --template 标志。这将为该子卷创建快照,并用其填充容器的根目录。
例如,要使用位于 /.snapshots/403/snapshot 的快照:
# systemd-nspawn --template=/.snapshots/403/snapshot -b -D my-container
其中 my-container 是将为容器创建的目录名。关机后,新创建的子卷将保留。
使用容器的临时 Btrfs 快照
可以使用 --ephemeral 或 -x 标志创建容器的临时 btrfs 快照,并将其用作容器根目录。引导进入容器时所做的任何更改都将丢失。例如:
# systemd-nspawn -D my-container -xb
其中 my-container 是现有容器或系统的目录。例如,如果 / 是一个 btrfs 子卷,可以通过以下方式为当前运行的宿主机系统创建一个临时容器:
# systemd-nspawn -D / -xb
容器关闭后,创建的 btrfs 子卷将立即被删除。
在 systemd-nspawn 中运行 docker
自 Docker 20.10 起,可以在启用了 cgroups v2(Arch Linux 中的默认设置)的非特权 systemd-nspawn 容器中运行 Docker 容器,而无需通过禁用 cgroups 和用户命名空间来削弱安全措施。为此,编辑 /etc/systemd/nspawn/myContainer.nspawn(如果不存在则创建)并添加以下配置。
/etc/systemd/nspawn/myContainer.nspawn
[Exec] SystemCallFilter=@keyring bpf
然后,Docker 应该在容器内正常工作。
使用最近版本的 systemd,您还需要以下临时解决方法:
/etc/systemd/nspawn/myContainer.nspawn
[Files] Bind=/proc:/run/proc Bind=/sys:/run/sys
更多详情请参阅 [19]。
由于 overlayfs 不支持用户命名空间且在 systemd-nspawn 内不可用,Docker 默认会退而使用效率低下的 vfs 作为其存储驱动程序,这会在每次启动容器时创建一个镜像副本。这可以通过使用 fuse-overlayfs 作为其存储驱动程序来解决。为此,我们首先需要向容器暴露 fuse:
/etc/systemd/nspawn/myContainer.nspawn
[Files] Bind=/dev/fuse
然后允许容器读写该设备节点:
# systemctl set-property systemd-nspawn@myContainer DeviceAllow='/dev/fuse rwm'
最后,在容器内安装 fuse-overlayfs。您需要重启容器以使所有配置生效。
映射本地用户并将他们的主目录绑定挂载到容器中
按照 #创建并引导一个最小化 Arch Linux 容器 中所述,在 /var/lib/machines/MyContainer/ 中创建一个容器。
使用 BindUser= 创建一个配置文件,将选定的本地用户名映射到容器中。请注意,这需要 PrivateUsers=,详情请参阅 systemd-nspawn(1)。在容器内外绑定挂载的主目录中创建的文件将具有相同的 UID 和 GID。
/etc/systemd/nspawn/MyContainer.nspawn
[Exec] User=username PrivateUsers=pick [Files] BindUser=username
在配置中包含 User= 指定了在容器内运行命令(如交互式 shell)时使用的默认用户:
# systemd-nspawn -M MyContainer bash
这对于从另一个 Linux 发行版测试 Arch Linux 软件包非常有用。
故障排除
execv(...) failed: Permission denied
尝试通过 systemd-nspawn -bD /path/to/container 引导容器(或在容器中执行某些操作)时,如果出现以下错误:
execv(/usr/lib/systemd/systemd, /lib/systemd/systemd, /sbin/init) failed: Permission denied
即使相关文件(如 /lib/systemd/systemd)的权限正确,这也可能是由于以非 root 用户身份挂载了存储容器的文件系统。例如,如果您通过 fstab 手动挂载磁盘,条目中包含 noauto,user,... 选项,则即使文件归 root 所有,systemd-nspawn 也不允许执行这些文件。
TERM 中的终端类型不正确(颜色异常)
通过 machinectl login 登录容器时,容器内终端的颜色和按键操作可能会异常。这可能是由于 TERM 环境变量中的终端类型不正确。除非另行明确配置,否则环境变量不会从宿主机的 shell 继承,而是退回到 systemd 中固定的默认值 (vt220)。要进行配置,请在容器内为 container-getty@.service(用于启动 machinectl login 的登录 getty)创建一个 systemd 配置覆盖,并将 TERM 设置为与您登录时所在宿主机终端匹配的值:
/etc/systemd/system/container-getty@.service.d/term.conf
[Service] Environment=TERM=xterm-256color
或者使用 machinectl shell。它能正确地从终端继承 TERM 环境变量。
在容器内挂载 NFS 共享
目前(2019 年 6 月)不可行。