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-based 发行版的引用,如 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[5] 的任何早于 9 (“Stretch”) 的版本,都会出现与“未知签名密钥”相关的问题,这使得后者成为在不获取不同的密钥环或禁用签名检查(如前所述)的情况下可安装的最旧版本。
debootstrap 无法解析虚拟软件包[6]的依赖关系,因此默认情况下不安装 systemd 的 dbus
和 libpam-systemd
建议的依赖项[7][8]。因此,一些 systemd/dbus 相关的功能(例如 localectl)以及使用 #machinectl 管理容器都无法开箱即用。
为了在基于 systemd 的系统中获得完整的功能,请将 --include=dbus,libpam-systemd
添加到 debootstrap 调用中,或者在容器中安装这些软件包
# debootstrap --include=dbus,libpam-systemd stable /path/to/machine
-b, --boot
选项)不使用 systemd 作为其 init 的容器时,可能会出现其他困难。自 Debian 8 (“jessie”)[9] 和 Ubuntu 15.04 (“vivid”)[10] 以来,它是 Debian 和 Ubuntu 的默认 init 系统。获取 shell 应该可以工作,而与 init 系统无关。与 Arch 一样,Debian 和 Ubuntu 也不会让您在没有密码的情况下登录。要设置 root 密码,请在不使用 -b
选项的情况下运行 systemd-nspawn
# systemd-nspawn -D /path/to/machine # passwd # logout
创建 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://getfedora.org/static/fedora.gpg [updates] name=Fedora $releasever - $basearch - Updates metalink=https://mirrors.fedoraproject.org/metalink?repo=updates-released-f$releasever&arch=$basearch gpgkey=https://getfedora.org/static/fedora.gpg
fedora.gpg 文件包含最新 Fedora 版本的 gpg 密钥 https://getfedora.org/security/。要设置最小化的 Fedora 37 容器
# mkdir /var/lib/machines/container-name # dnf --releasever=37 --best --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 包含额外的应用程序、语言包等,Extras 包含 RHEL 中未包含的软件包。因此,对于最小化的容器,我们只需要将 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
构建和测试软件包
有关示例用法,请参阅 Creating packages for other distributions。
管理
位于 /var/lib/machines/
中的容器可以通过 machinectl 命令进行控制,该命令在内部控制 systemd-nspawn@.service
单元的实例。/var/lib/machines/
中的子目录对应于容器名称,即 /var/lib/machines/container-name/
。
/var/lib/machines/
中,则可以对其进行符号链接。有关详细信息,请参阅 machinectl(1) § FILES AND DIRECTORIES。默认 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 子命令 容器名称
命令进行管理。例如,要启动一个容器
$ machinectl start container-name
machinectl start container_name
将导致错误 Invalid machine name container_name
。有关更多详细信息,请参阅 [12] 和 [13]。类似地,还有诸如 poweroff
、reboot
、status
和 show
等子命令。有关详细说明,请参阅 machinectl(1) § Machine Commands。
poweroff
和 reboot
命令从容器内执行关机和重启操作。其他常用命令有
machinectl list
– 显示当前正在运行的容器列表machinectl login 容器名称
– 在容器中打开交互式登录会话machinectl shell [用户名@]容器名称
– 在容器中打开交互式 shell 会话(这将立即调用用户进程,而无需经过容器中的登录过程)machinectl enable 容器名称
和machinectl disable 容器名称
– 启用或禁用容器在启动时启动,有关详细信息,请参阅 #启用容器在启动时启动
machinectl 还具有用于管理容器(或虚拟机)镜像和镜像传输的子命令。有关详细信息,请参阅 machinectl(1) § Image Commands 和 machinectl(1) § Image Transfer Commands。截至 2023 年第一季度,machinectl(1) § EXAMPLES 中的前 3 个示例演示了镜像传输命令。machinectl(1) § FILES AND DIRECTORIES 讨论了在哪里可以找到合适的镜像。
systemd 工具链
systemd 核心工具链的大部分已更新为与容器一起工作。这样做工具通常提供 -M, --machine=
选项,该选项将容器名称作为参数。
示例
查看特定机器的日志
# journalctl -M container-name
显示控制组内容
$ systemd-cgls -M container-name
查看容器的启动时间
$ systemd-analyze -M container-name
资源使用概览
$ systemd-cgtop
配置
每个容器的设置
要指定每个容器的设置而不是全局覆盖,可以使用 .nspawn 文件。有关详细信息,请参阅 systemd.nspawn(5)。
启用容器在启动时启动
当频繁使用容器时,您可能希望在启动时启动它。
首先确保 machines.target
已启用。
可以通过 machinectl 发现的容器可以启用或禁用
$ machinectl enable container-name
- 这具有启用
systemd-nspawn@容器名称.service
systemd 单元的效果。 - 如 #默认 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@容器名称.service.d/
中创建永久文件。
根据文档,MemoryHigh
是保持内存消耗在控制范围内的首选方法,但它不会像 MemoryMax
那样受到硬性限制。您可以同时使用这两个选项,将 MemoryMax
作为最后一道防线。还要考虑到您不会限制容器可以看到的 CPU 数量,但通过限制容器在最大程度上获得的时间,相对于总 CPU 时间,您将获得类似的结果。
--runtime
选项。您可以使用 systemd-cgtop 检查其结果。网络
systemd-nspawn 容器可以使用主机网络或私有网络
- 在主机网络模式下,容器可以完全访问主机网络。这意味着容器将能够访问主机上的所有网络服务,并且来自容器的数据包将以外部网络来自主机(即共享相同的 IP 地址)的形式出现。
- 在私有网络模式下,容器与主机的网络断开连接。这使得所有网络接口都不可用于容器,但环回设备和显式分配给容器的接口除外。有多种不同的方法可以为容器设置网络接口
- 可以将现有接口分配给容器(例如,如果您有多个以太网设备):请参阅#使用现有接口。
- 可以创建与现有接口关联的虚拟网络接口(即VLAN 接口)并将其分配给容器:请参阅#使用 “macvlan” 或 “ipvlan” 接口。
- 可以在主机和容器之间创建虚拟以太网链接:请参阅#使用虚拟以太网链接。
主机网络模式适用于应用程序容器,这些容器不运行任何会配置分配给容器的接口的网络软件。当您从 shell 运行 systemd-nspawn 时,主机网络是默认模式。
另一方面,私有网络模式适用于系统容器,这些容器应与主机系统隔离。虚拟以太网链接的创建是一种非常灵活的工具,允许创建复杂的虚拟网络。这是由 machinectl 或 systemd-nspawn@.service
启动的容器的默认模式。
以下子章节描述了常见场景。有关可用 systemd-nspawn 选项的详细信息,请参阅systemd-nspawn(1) § Networking Options。
主机网络
要禁用私有网络以及由 machinectl 启动的容器使用的虚拟以太网链接的创建,请添加具有以下选项的 .nspawn 文件
/etc/systemd/nspawn/container-name.nspawn
[Network] VirtualEthernet=no
这将覆盖 systemd-nspawn@.service
中使用的 -n
/--network-veth
选项,新启动的容器将使用主机网络模式。
私有网络
使用虚拟以太网链接
如果容器使用 -n
/--network-veth
选项启动,systemd-nspawn 将在主机和容器之间创建虚拟以太网链接。链接的主机端将作为名为 ve-容器名称
的网络接口提供。链接的容器端将命名为 host0
。请注意,此选项隐含 --private-network
。
- 如果容器名称太长,则接口名称将被缩短(例如,
ve-long-conKQGh
而不是ve-long-container-name
)以适应 15 个字符的限制。完整名称将设置为接口的altname
属性(请参阅 ip-link(8)),并且仍然可以用于引用该接口。 - 当使用
ip link
检查接口时,接口名称将显示后缀,例如ve-容器名称@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-容器名称
接口并启动 DHCP 服务器,该服务器为主机接口以及容器分配 IP 地址, - 容器中的
/usr/lib/systemd/network/80-container-host0.network
文件匹配host0
接口并启动 DHCP 客户端,该客户端从主机接收 IP 地址。
systemd-networkd
将提供所描述的容器网络自动配置,而不会干扰现有的网络设置,因为现有接口将保持为 systemd-networkd
unmanaged 状态,除非另行配置。因此,它与托管网络设置(例如使用 NetworkManager)完全兼容,并且启动 systemd-networkd
实际上是使容器内部获得 Internet 连接(以及能够从主机访问它们)所需的全部操作,而不会干扰主机上的任何内容。如果您不使用 systemd-networkd,您可以在主机接口上配置静态 IP 地址或启动 DHCP 服务器,并在容器中启动 DHCP 客户端。有关详细信息,请参阅 网络配置。
使用 NAT 网络
要使容器能够访问外部网络,您可以按照 Internet sharing#Enable 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
表必须手动配置,如 Internet sharing#Enable 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=桥接名称
选项完成的。请注意,--network-bridge
隐含了 --network-veth
,即虚拟以太网链接是自动创建的。但是,链接的主机端将使用 vb-
前缀而不是 ve-
,因此 systemd-networkd 用于启动 DHCP 服务器和 IP 伪装的选项将不会应用。
桥接管理留给管理员处理。例如,桥接可以将虚拟接口与物理接口连接,或者它可以仅连接多个容器的虚拟接口。有关使用 systemd-networkd 的示例配置,请参阅 systemd-networkd#Network bridge with DHCP 和 systemd-networkd#Network bridge with static IP addresses。
还有一个 --network-zone=区域名称
选项,它类似于 --network-bridge
,但网络桥接由 systemd-nspawn 和 systemd-networkd 自动管理。当配置了 --network-zone=区域名称
的第一个容器启动时,会自动创建名为 vz-区域名称
的桥接接口,并在配置了 --network-zone=区域名称
的最后一个容器退出时自动删除。因此,此选项可以轻松地将多个相关容器放置在公共虚拟网络上。请注意,vz-*
接口由 systemd-networkd 管理,方式与使用 /usr/lib/systemd/network/80-container-vz.network
文件中的选项管理 ve-*
接口相同。
使用 “macvlan” 或 “ipvlan” 接口
与创建虚拟以太网链路(其主机端可能添加到桥接,也可能不添加)不同,你可以在现有的物理接口(即 VLAN 接口)上创建虚拟接口,并将其添加到容器中。虚拟接口将与底层主机接口桥接,因此容器将暴露于外部网络,这使其可以通过 DHCP 从与主机连接的同一 LAN 获取不同的 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 规则来实现,但需要手动配置 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 在主机上运行,则其存根
resolv.conf
文件将被复制或绑定挂载到容器中。 - 否则,
/etc/resolv.conf
文件将从主机复制或绑定挂载到容器。
在最后两种情况下,如果容器根目录可写,则复制该文件;如果容器根目录只读,则绑定挂载该文件。
对于 systemd-resolved 在主机上运行的第二种情况,systemd-nspawn 期望它也在容器中运行,以便容器可以使用来自主机的存根符号链接文件 /etc/resolv.conf
。如果不是这样,默认值 auto
将不再起作用,你应该使用 replace-*
选项之一替换该符号链接。
技巧与提示
运行非 shell/init 命令
- [选项]
--as-pid2
[调用] shell 或指定的程序作为进程 ID (PID) 2 而不是 PID 1 (init)。[...] 建议使用此模式在容器中调用任意命令,除非它们已被修改为作为 PID 1 正确运行。或者换句话说:此开关应该用于几乎所有命令,除非该命令引用 init 或 shell 实现 [...] 此选项不能与--boot
组合使用。
非特权容器
systemd-nspawn 支持非特权容器,但容器需要以 root 身份启动。
最简单的方法是让 systemd-nspawn 使用 -U
选项自动选择未使用的 UID/GID 范围
# systemd-nspawn -bUD ~/MyContainer
如果内核支持用户命名空间,则 -U
选项等效于 --private-users=pick --private-users-ownership=auto
。有关详细信息,请参阅 systemd-nspawn(1) § 用户命名空间选项。
如果容器已使用 --private-users-ownership=chown
选项(或在使用 -U
需要 --private-users-ownership=chown
的文件系统上)以私有 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
不需要其他绑定。
在某些情况下(主要是与 -b
一起使用时),你可能仍然需要手动在容器中设置 DISPLAY=:1
。
运行 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 <username>
选项。或者,你可以启动容器,并让例如 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 图形,可能需要将 /dev/dri
绑定挂载到容器,方法是在 .nspawn 文件中添加以下行
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 系统上运行
请参阅 Init#systemd-nspawn。
使用 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 的非特权 systemd-nspawn 容器(Arch Linux 中的默认设置)中运行 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(...) 失败:权限被拒绝
当尝试通过 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
),除非显式配置。要进行配置,请在容器内为启动 machinectl login
的登录 getty 的 container-getty@.service
systemd 服务创建一个配置覆盖,并将 TERM
设置为与你从中登录的主机终端匹配的值
/etc/systemd/system/container-getty@.service.d/term.conf
[Service] Environment=TERM=xterm-256color
或者使用 machinectl shell
。它正确地从终端继承 TERM
环境变量。
目前不可能(2019 年 6 月)。