systemd-nspawn

来自 Arch Linux 中文维基

systemd-nspawnchroot 命令类似,是个终极版的 chroot。

systemd-nspawn 可以在轻量的命名空间容器中运行命令或系统。它比 chroot 强大的地方是,它完全虚拟了文件系统层次结构、进程树、各种 IPC 子系统以及主机名。

systemd-nspawn 将容器中各种内核接口的访问限制为只读,像是 /sys, /proc/sys/sys/fs/selinux。网络接口和系统时钟不能从容器内更改,不能创建设备节点。不能从容器中重启宿主机,也不能加载内核模块。

相比 LXCLibvirt, systemd-nspawn 更容易配置。

安装[编辑 | 编辑源代码]

systemd-nspawn 是 systemd 的一部分并被打进 systemd 软件包中。

用例[编辑 | 编辑源代码]

创建并启动最小 Arch Linux 容器[编辑 | 编辑源代码]

首先安装 arch-install-scripts

然后,创建一个目录来保存容器。在这个用例中我们将使用 ~/MyContainer

接下来,我们使用 pacstrap 在容器中安装一个基本的Arch系统。其中至少需要安装包组 base

# pacstrap -K -c ~/MyContainer base [additional packages/groups]
提示:base 已经不再依赖内核包 linux,这已为容器化做好准备。
注意: 如果是从一个 pacstrap 不可用的操作系统中创建,可以使用如下方法替代容器镜像 从现有_Linux_发行版安装_Arch_Linux#方法一:使用_Bootstrap_镜像(推荐)。pacman 密钥环需要在容器中进行初始化,参阅 从现有_Linux_发行版安装_Arch_Linux#初始化_pacman_密匙环

一旦安装完成后,进入容器中并设置root密码:

# systemd-nspawn -D ~/MyContainer
# passwd
# logout
提示:设置根密码是可选的。你可以运行 machinectl shell root@MyContainer 直接获取一个 root shell ,并不需要登录。

最后, 启动容器:

# systemd-nspawn -b -D ~/MyContainer

参数 -b 将会启动这个容器(比如:以 PID=1 运行 systemd), 而不是仅仅启动一个 shell, 而参数 -D 指定成为容器根目录的目录。

容器启动后,输入密码以"root"身份登录。

注意: 如果登录失败显示 "Login incorrect", 问题可能是 securetty TTY 设备白名单导致的。 请查看#Root登录失败

可以在容器内运行 poweroff 来关闭容器。在主机端,容器可以通过 machinectl 工具进行控制。

注意: 要从容器内终止 session,请按住 Ctrl 并快速地按 ] 三下。

创建 Debian 或 Ubuntu 环境[编辑 | 编辑源代码]

安装 debootstrap 以及 debian-archive-keyringubuntu-keyring 中的一个或两个(当然要安装你想要的发行版的keyring)。

然后通过以下结构式调用 deboostrap

# debootstrap codename container-name repository-url
  • codename:对于 Debian,有效的代码名称要不然是稳定性别名如 stabletestingunstable,要不然就是版本名称如 bookwormsid。你可以从这里获取名称列表:[1]。对于 Ubuntu:只有像是 jammynoble 这样的名称应该被使用而不是版本号。你可以从 [2] 或者 [3] 获取一份代码名称与版本号的对应表格。
  • container-name 是会包含操作系统文件树的目录,如果它不存在的话将会被创建。
注意:
  • 如果你需要一个以归档的 Debian 版本(指的是任何在2024年8月,Debian 10/Buster 之前的版本),请使用一个特殊的 debian-archive 镜像地址:https://archive.debian.org/debian/ 。Ubuntu 应该使用 https://old-releases.ubuntu.com/ubuntu/
  • systemd-nspawn 要求容器中的操作系统使用 systemd 作为其 init 程序(以 PID 1 运行)。从 Debian 8 (“jessie”) [5] 以及 Ubuntu 15.04 (“vivid”)[6] 之后,这是两者的默认 init 程序。但请注意,对于不包含在上述密钥包中的版本,会出现与 “未知签名密钥(unknown signing key)”相关的问题,比如任何早于 Debian 9 (“Stretch”)[7] 的发行版本。这使得后者成为成为无需额外操作即可安装的最老版本。

与 Arch 相同,Debian 和 Ubuntu 不会让您在首次登录时无密码登录。为了设置root密码登录,要不使用"-b"参数并设置密码:

# cd /var/lib/machines
# systemd-nspawn -D ./container-name
# passwd
# logout
提示:如果计划使用 #machinectl 管理容器,请确保使用合适的包名在目标操作系统容器中安装 dbus 包,否则容器将不会工作。可以安装 systemd-container 时使用 --include= 选项,这始终是 dbus 和 systemd 的依赖项:
# debootstrap --include=systemd-container codename /var/lib/machines/container-name repository-url

Create a Fedora or AlmaLinux environment[编辑 | 编辑源代码]

Install dnf, and edit the /etc/dnf/dnf.conf file to add the required Fedora repositories.

/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

The fedora.gpg file contain the gpg keys for the latest Fedora releases https://getfedora.org/security/. To set up a minimal Fedora 37 container:

# 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
注意: If you want to install a different Fedora release, keep in mind that different releases will have distinct package requirements.

If you are using btrfs filesystem create a subvolume instead of creating a directory.

An Enterprise Linux derivative like AlmaLinux has three repositories enabled by default, BaseOS wich contains a core set that provides the basis for all installations, AppStream that includes additional applications, language packages, etc and Extras that contains packages not included in RHEL. So for a minimal container we only need to add the BaseOS repository to /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

To create an AlmaLinux 9 minimal container:

# 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

This will install the latest minor version of AlmaLinux 9, you can choose to install a specific point release, but you will need to change the gpgpkey entry to manually point to RPM-GPG-KEY-AlmaLinux-9

Just like Arch, Fedora or AlmaLinux will not let you log in as root without a password. To set up the root password, run systemd-nspawn without the -b option:

# 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 选项[编辑 | 编辑源代码]

要明白非常重要的一点是通过 machinectlsystemd-nspawn@.service 启动的容器所使用的默认选项与通过 systemd-nspawn 命令手动启动的有所不同。 通过服务启动所使用的额外选项有:

  • -b/--boot – 管理的容器会自动搜索一个init程序,并以PID 1的形式调用它。
  • --network-veth 关联于 --private-network – 管理的容器获得一个虚拟网络接口,并与主机网络断开连接。 详情看 #网络
  • -U – 如果内核支持,管理的容器默认使用 user_namespaces(7) 特性。 解释请看 #无特权容器
  • --link-journal=try-guest

这些行为可以在每个容器配置文件中被覆盖, 详情看 #配置

machinectl[编辑 | 编辑源代码]

注意: machinectl 工具要求在容器中安装 systemddbus 。有关详细讨论,请参阅 [8]

容器可以被 machinectl subcommand container-name 命令管理, 比如说,启动容器:

$ machinectl start container-name
注意: machinectl 要求 container-name 只包含 ASCII 字符,数字与连字符。这样才能保证 hostname 的合法性。比如,如果 container-name 包含了下划线,这将不会被识别并且当你运行 machinectl start container_name 时将会报错 Invalid machine name container_name。详情请看 [9][10]

相似的, 还有其他的子命令 poweroffrebootstatusshow. 详细解释请看 machinectl(1) § Machine Commands

提示:关机与重启操作可以在容器内通过 poweroff and reboot 命令执行。

其他常见命令:

  • machinectl list – 显示现在正在运行的容器
  • machinectl login container-name – 在一个容器中打开一个交互式登录会话
  • machinectl shell [username@]container-name – 在容器中打开一个交互式shell会话(这将立即调用一个用户进程,而不需要通过容器中的登录进程)
  • machinectl enable container-namemachinectl disable container-name – 启用或禁用容器的随开机启动,详见 #启用容器的随开机启动

machinectl 也有管理容器(或虚拟机)镜像和镜像传输的子命令。 详情见 machinectl(1) § Image Commandsmachinectl(1) § Image Transfer Commands

这篇文章的某些内容需要扩充。

原因: 增加一些明确的例子,如何使用镜像传输命令。最重要的是,在哪里可以找到合适的镜像。 (在 Talk:Systemd-nspawn 中讨论)

systemd 工具链[编辑 | 编辑源代码]

大部分核心的 systemd 工具链已更新为对容器有效。工具们通常提供了 -M, --machine= 选项并把容器名称作为参数。 用例:

查看特定容器的 journal 日志:

# journalctl -M MyContainer

显示控制组的内容:

$ systemd-cgls -M MyContainer

查看容器的启动时间:

$ systemd-analyze -M MyContainer

查看资源使用情况的概况:

$ systemd-cgtop

配置[编辑 | 编辑源代码]

容器前设置[编辑 | 编辑源代码]

要指定容器前设置而不是全局覆盖,可以使用.nspawn文件。 详情见 systemd.nspawn(5)

注意:
  • 当执行machinectl remove 的时候,.nspawn 文件可能会从 /etc/systemd/nspawn/ 被意外移除。 [11]
  • 当有--settings=override(在systemd-nspawn@.service文件内被指定)时,.nspawn内与命令行中所指定的为网络选项的交互不能正常工作。[12] 作为一个变通方法,你需要引入VirtualEthernet=on选项,即使服务指定了--network-veth

启用容器的随开机启动[编辑 | 编辑源代码]

当频繁使用一个容器时,你可能希望在系统启动时启动它。

首先确保 machines.target 已经 enabled

容器被 machinectl 的可发现可以 enabled 或者 disabled。

$ machinectl enable container-name
注意:
  • 这样做的效果是启用 systemd-nspawn@container-name.service systemd单元。
  • #默认 systemd-nspawn 选项中提到的,由machinectl启动的容器会获得一个虚拟的以太网接口。要禁用私有网络,请参见#使用主机网络

资源控制[编辑 | 编辑源代码]

您可以使用控制组来使用 systemctl set-property 实现对容器的限制和资源管理,请参阅 systemd.resource-control(5)。例如,您可能希望限制内存量或 CPU 使用率。要将容器的内存消耗限制为 2 GiB:

# systemctl set-property systemd-nspawn@myContainer.service MemoryMax=2G

或者将 CPU 时间使用率限制为大约相当于 2 个内核:

# systemctl set-property systemd-nspawn@myContainer.service CPUQuota=200%

这将在 /etc/systemd/system.control/systemd-nspawn@myContainer.service.d/ 中创建常驻文件}。

根据文档,MemoryHigh 是控制内存消耗的首选方法,但也不会像 MemoryMax 那样进行强制限制。您可以使用这两个选项将 MemoryMax 作为您最后的一道防线。还要考虑到如果您不会限制容器可以“看到”的 CPU 数量,但通过限制容器相对于总 CPU 时间的最大时间,也可以获得类似的结果。

提示:如果不希望在重新启动后保留此更改,则可以传递选项 --runtime 使更改成为临时性的。您可以使用 systemd-cgtop 检查其结果。

网络[编辑 | 编辑源代码]

systemd-nspawn 容器可以使用主机网络或者私有网络:

  • 在主机网络模式下,容器可以完全访问主机网络。这意味着容器将能够访问主机上的所有网络服务,来自容器的数据包将在外部网络中显示为来自主机(即共享同一IP地址)。
  • 在私有网络模式下,容器与主机的网络断开连接,这使得容器无法使用所有网络接口,但环回设备和明确分配给容器的接口除外。为容器设置网络接口有多种不同的方法:
    • 可以将现有接口分配给容器(例如,如果您有多个以太网设备)。
    • 可以创建一个与现有接口(即VLAN接口)相关联的虚拟网络接口,并将其分配给容器。
    • 可以创建主机和容器之间的虚拟以太网链接。
在后一种情况下,容器的网络是完全隔离的(与外部网络以及其他容器),由管理员来配置主机和容器之间的网络。这通常涉及创建一个网桥network bridge来连接多个(物理或虚拟)接口,或者在多个接口之间设置一个NATNetwork Address Translation

主机网络模式适用于应用程序容器,它不运行任何网络软件来配置分配给容器的接口。当你从shell运行systemd-nspawn时,主机联网是默认模式。

另一方面,私有网络模式适用于应与主机系统隔离的 "系统容器"。创建虚拟以太网链路是一个非常灵活的工具,可以创建复杂的虚拟网络。这是由machinectlsystemd-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-容器名@if2host0@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,可以配置静态 IP 地址或在主机接口上启动 DHCP 服务器,在容器中启动 DHCP 客户端。详情请参见网络配置

要让容器访问外部网络,您可以按照 Internet sharing#Enable NAT 中的描述配置 NAT。如果您使用 systemd-networkd,这将通过 /usr/lib/systemd/network/80-container-ve.network 中的 IPMasquerade=yes 选项自动(部分)完成。然而,这只会发出一个iptables规则,比如说:

-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
注意: systemd-networkd使用libiptc库与iptables交互。如果您使用nftables,请安装 iptables-nft 转译层。参见 systemd issue 13307

并且,为了接收DHCP服务器的入站连接,你需要在 ve-+ 接口上开放 UDP 67 端口(由 systemd-networkd 操控):

# iptables -A INPUT -i ve-+ -p udp -m udp --dport 67 -j ACCEPT

使用网络桥接[编辑 | 编辑源代码]

如果您已在主机系统上配置了网桥network bridge,则可以为容器创建一个虚拟以太网链路,并将其主机侧添加到网桥中。这可以通过--network-bridge=bridge-name选项来完成。请注意,--network-bridge意味着--network-veth,即虚拟以太网链路是自动创建的。然而,链路的主机端将使用vb-前缀而不是ve-,因此用于启动DHCP服务器和IP伪装的systemd-networkd选项将不会被应用。

网桥的管理由管理员负责。例如,网桥可以用一个物理接口连接虚拟接口,也可以只连接几个容器的虚拟接口。参见systemd-networkd#Network bridge with DHCPsystemd-networkd#Network bridge with static IP addresses,了解使用systemd-networkd进行配置的例子。

还有一个--network-zone=zone-name选项,它与--network-bridge类似,但网桥由systemd-nspawnsystemd-networkd自动管理。当第一个用vz-zone-name配置的容器启动时,会自动创建名为--network-zone=zone-name的网桥接口,当最后一个用--network-zone=zone-name配置的容器退出时,会自动删除。因此,这个选项可以方便地将多个相关的容器放置在一个共同的虚拟网络上。请注意,vz-*接口由systemd-networkd管理的方式与ve-*接口相同,使用/usr/lib/systemd/network/80-container-vz.network文件中的选项。

使用 "macvlan" 或者 "ipvlan" 接口[编辑 | 编辑源代码]

您可以在现有的物理接口(即VLAN接口)上创建一个虚拟接口,并将其添加到容器中,而不是创建一个虚拟的以太网链路(其主机端可能被添加到桥接中,也可能没有)。该虚拟接口将与底层主机接口进行桥接,从而使容器暴露在外部网络中,从而使其能够通过DHCP从与主机相连的同一局域网中获得一个独特的IP地址。

systemd-nspawn 提供两个选项:

  • --network-macvlan=interface – 虚拟接口的MAC地址将与底层物理interface不同,并被命名为mv-interface
  • --network-ipvlan=interface – 虚拟接口的MAC地址将与底层物理interface相同,并命名为iv-interface。​

所有选项都意味着 --private-network.

使用现有接口[编辑 | 编辑源代码]

如果主机系统有多个物理网络接口,可以使用--network-interface=interfaceinterface分配给容器(并使它在容器启动时对主机不可用)。请注意,--network-interface意味着--private-network

注意: 目前不支持将无线网络接口传递给systemd-nspawn容器。[13]

端口映射[编辑 | 编辑源代码]

当启用私有网络时,可以使用-p/-port选项或使用.nspawn文件中的Port设置将主机上的各个端口映射到容器上的端口。这可以通过向nat表发出iptables规则来完成,但需要手动配置forward表中的filter链,如#使用虚拟以太网链接所示。

例如,将主机上的TCP端口8000映射到容器中的TCP端口80:

/etc/systemd/nspawn/container-name.nspawn
[Network]
Port=tcp:8000:80
注意:
  • systemd-nspawn在映射端口时明确地排除了loopback接口。因此,在上面的例子中,localhost:8000 连接到主机而不是容器。只有到其他接口的连接才会受到端口映射的影响。详情请参见 [14]
  • 端口映射只对IPv4连接有效。[15]

域名解析[编辑 | 编辑源代码]

容器中的域名解析 Domain name resolution 可以以主机系统中相同的方式配置。另外,systemd-nspawn 提供了管理容器中 /etc/resolv.conf 文件的选项:

  • --resolv-conf can be used on command-line
  • ResolvConf= can be used in .nspawn files

这些相应的选项有许多可能的值,在 systemd-nspawn(1) § Integration Options 的描述中会提到。 默认值为 auto,这意味着:

  • 如果启用了--private-network/etc/resolv.conf就会保持容器中的原样。
  • 否则,如果systemd-resolved在主机上运行,它的存根文件resolv.conf将被复制或绑定到容器中。
  • 否则,/etc/resolv.conf 文件就会被从主机复制或绑定到容器上。

在后两种情况下,如果容器根部是可写的,则复制文件,如果是只读的,则绑定挂载。

对于第二种 systemd-resolved 在宿主机上运行的情况, systemd-nspawn 希望它也能在容器中运行,所以容器将会从宿主中 /etc/resolv.conf 文件建立桩符号链接。如果不是这样,那么默认值 auto 将不会生效,你应该使用任意 replace-* 选项来替换这个链接。

提示和技巧[编辑 | 编辑源代码]

运行 non-shell/init 命令[编辑 | 编辑源代码]

来自 systemd-nspawn(1) § Execution_Options:

[选项] --as-pid2 [调用] shell 或以进程ID (PID) 2 而不是 PID 1 (init) 来运行特定程序。 [...] 建议使用这种模式来调用容器中的任意命令,除非它们已经被修改为以PID 1的形式正确运行。或者换句话说:这个改变应该被用于几乎所有的命令,除非该命令是指一个init或shell的实现。

无特权容器[编辑 | 编辑源代码]

systemd-nspawn支持无特权的容器,不过容器需要以root身份启动。​

本文或本章节的语言、语法或风格需要改进。参考:帮助:风格

原因:Very little of Linux Containers#Enable support to run unprivileged containers (optional) applies to systemd-nspawn.(在Talk:Systemd-nspawn讨论)

最简单的方法是通过-U选项让systemd-nspawn自动选择一个未使用的UID/GID范围。

# systemd-nspawn -bUD ~/MyContainer

如果内核支持用户命名空间,-U选项相当于--private-users=pick --private-users-ownership=auto。详情请参见systemd-nspawn(1) § User Namespacing Options

注意: 您也可以手动指定容器的UID/GID范围,但是,这很少有用。

如果一个容器曾使用 --private-users-ownership=chown 选项(或者在一个 -U 需要 --private-users-ownership=chown 的文件系统中)在私有UID/GID范围启动,就需要一直这样使用它以避免权限错误。另外,也可以在一个ID范围从0开始的容器文件系统中撤销--private-users-ownership=chown的效果:

# systemd-nspawn -D ~/MyContainer --private-users=0 --private-users-ownership=chown

使用 X 环境[编辑 | 编辑源代码]

本文或本章节的事实准确性存在争议。

原因: 本节末尾有关 systemd 版本的注释似乎已经过时。对于我(译者注:原作者)来说(systemd 版本为239) 如果 /tmp/.X11-unix 权限是 rw,X应用程序可以工作运行。(在 Talk:Systemd-nspawn#/tmp/.X11-unix contents have to be bind-mounted as read-only - still relevant? 中讨论)


详情见 XhostChange root#Run graphical applications from chroot.

您需要设置您容器会话中 DISPLAY 以连接到外部 X 服务器。

X 在 /tmp 文件夹中存储一些必要的文件。为了使容器能显示任何内容,它需要访问这些文件。为此,当启动容器时,请追加 --bind-ro=/tmp/.X11-unix 选项。

注意: 从 systemd 版本 235 开始, /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",GID为65535,会使输入匹配你的每一个显示。 更多信息请参考 Xsecurity(7)

使用 X 嵌套/Xephyr[编辑 | 编辑源代码]

另一个运行X应用程序并避免共享X桌面风险的简单方法是使用X嵌套。 这里的优点是完全避免了容器内应用程序和非容器内应用程序之间的交互,并且能够运行不同的 desktop environmentwindow manager,缺点是使用 Xephyr 时性能较差,并且缺乏硬件加速。 在容器外启动 Xephyr:

# Xephyr :1 -resizeable

然后在使用下面的参数启动容器:

--setenv=DISPLAY=:1 --bind-ro=/tmp/.X11-unix/X1

不需要其他绑定。 在某些情况下,你可能仍然需要在容器中手动设置 DISPLAY=:1(主要是在与 b 一起使用时)。

运行 Firefox[编辑 | 编辑源代码]

以PID 1运行​

 # systemd-nspawn --setenv=DISPLAY=:0 \
              --setenv=XAUTHORITY=~/.Xauthority \
              --bind-ro=$HOME/.Xauthority:/root/.Xauthority \
              --bind=/tmp/.X11-unix \
              -D ~/containers/firefox \
              --as-pid2 \
              firefox
              
注意: 因此,如果不使用 #无特权容器,firefox会以根用户的身份运行,这就带来了自身的风险。在这种情况下,你可以先选择在容器内 添加一个用户,然后在 systemd-nspawn 调用中增加 --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图形,可能有必要在 .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

你可以通过运行 glxinfoglxgears 来确认它已经被启用。

Nvidia GPUs[编辑 | 编辑源代码]

如果你不能在容器上安装与主机上相同的nvidia驱动版本,你可能需要同时绑定驱动库文件。你可以在主机上运行 pacman -Ql nvidia-utils 来查看它包含的所有文件。你不需要把所有文件都复制过来。当容器通过 machinectl start container-name 运行时,下面的systemd覆盖文件将把所有必要的文件绑定过来。

本文或本章节的事实准确性存在争议。

原因:/usr/lib/ 绑定到 /usr/lib/x86_64-linux-gnu/ 是没有必要的。(在 Talk:Systemd-nspawn 中讨论)


/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
注意: 每当你在主机上升级nvidia驱动时,你将需要重启容器,并可能需要在容器中运行 ldconfig 以更新库。

访问主机文件系统[编辑 | 编辑源代码]

请见 --bind--bind-rosystemd-nspawn(1).

如果主机和容器都是 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 subvolume 作为容器根目录的模板,请使用 --template 标志。这将获取子卷的快照,并以它填充容器的根目录。

注意: 如果指定的模板路径不是子卷的根,则会复制整个树。这将非常耗时。​

例如,要使用位于/.snapshots/403/snapshot的快照:

# systemd-nspawn --template=/.snapshots/403/snapshots -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=add_key keyctl bpf

然后,Docker 应该在容器内按原样工作。

注意: 上述配置将系统调用 add_keykeyctlbpf 暴露给容器,而这些调用是没有命名间隔的。这仍然可能是一个安全风险,尽管它比完全禁用用户命名空间要低得多,就像人们在 cgroups v2 之前必须做的那样。

由于 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 包。你需要重新启动容器以使所有配置生效。

疑难解答[编辑 | 编辑源代码]

Root登录失败[编辑 | 编辑源代码]

如果您在尝试登录时(即使用machinectl login <name>)得到以下错误:

arch-nspawn login: root
Login incorrect

并且容器中journal显示:

pam_securetty(login:auth): access denied: tty 'pts/0' is not secure !


本文或本章节的事实准确性存在争议。

原因: 用户不应编辑 /usr/lib 中的文件,/usr/lib/tmpfiles.d/arch.conf 中的改动会在 filesystem 升级后丢失。(在 Talk:Systemd-nspawn 中讨论)


只删除容器文件系统中的/etc/securetty[16]/usr/share/factory/etc/securetty ,或者也可以根据需要只简单地向容器文件系统中 /etc/securetty 添加所需的pty终端设备(如 pts/0)。

但是任何改变都会在下一次启动时被覆盖,因此有必要同时在容器文件系统上的 /usr/lib/tmpfiles.d/arch.conf 文件中删除 /etc/securetty 条目,见FS#63236。如果你选择删除,也可以在 /etc/pacman.conf 中选择将这些文件列入黑名单(NoExtract),以防止它们被重新安装。详见 FS#45903

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,...选项的磁盘,systemd-nspawn将不允许执行文件,即使它们是由root用户拥有的。

TERM中的终端类型不正确(破损的颜色)[编辑 | 编辑源代码]

当通过machinectl login登录容器时,容器内的终端中的颜色和按键可能会被破坏。这可能是由于TERM环境变量中的终端类型不正确。除非明确配置,否则环境变量不会从主机上的 shell 继承,而是回到 systemd 中固定的默认值 (vt220)。要配置,在容器内为 container-getty@.service systemd 服务创建一个配置覆盖,启动 machinectl login 的登录 getty,并将 TERM 设置为与您登录的主机终端匹配的值:

/etc/systemd/system/container-getty@.service.d/term.conf
[Service]
Environment=TERM=xterm-256color

或者使用machinectl shell。它正确地继承了终端的TERM环境变量。

在容器内挂载NFS共享[编辑 | 编辑源代码]

目前(2019年6月)还不可能

这篇文章的某些内容需要扩充。

原因: 下述方法未经过验证,正在讨论 (在 Talk:Systemd-nspawn#A_trick_way_to_mount_a_NFS_share_with_the_container 中讨论)

曲线救国的方式是使用主机挂载NFS后通过目录映射到容器目录,NFS挂载方式请查看 NFS#Client。挂载后会遇到权限错误,这是因为容器的用户ID是通过主机映射过去的,详情请查看:[17], 解决这个问题的方法有很多种,如下面几种:

  1. 在主机上使用管理员权限将NFS挂载目录所属用户ID修改为容器所对应的实际用户ID。
  2. 在容器启动时使用--private-users参数指定使用用户作为特权用户启动。
  3. 在挂载目录时使用--bind-user参数指定NFS目录所属用户映射到容器中。

See also[编辑 | 编辑源代码]