跳到正文
Hacker News· ibobev·· 21 小时前AI 评分22

systemd-nspawn 容器入门指南:用 chroot 的“加强版”跑 LLM

A whirlwind tour of systemd-nspawn containers (2025)

AI 导读

systemd-nspawn 是基于 Linux namespaces 的轻量容器技术,与 Docker 不同,它通常使用可写根文件系统,更像虚拟机,可视为 chroot 的加强版。

正文 · AI 翻译

在去年的年度更新中,我谈到了如何将运行在Ollama以及Open WebUI中的自托管 LLM 隔离在systemd-nspawn容器里,并承诺会写一篇相关的博客文章。然而,在撰写那篇博客时,一个关于我为何使用它而非 Docker 的脚注,意外地演变成了一篇独立的完整博客文章。以下是关于systemd-nspawn的正式文章。

从根本上说,systemd-nspawn是一种基于 Linux 命名空间的轻量级容器技术,与Docker并无太大不同。区别主要在于镜像管理——systemd-nspawn不是通过Dockerfile来描述如何构建镜像并分发包含即用软件、只读的预构建镜像,而是通常与可写的根文件系统一起使用,其运作方式更类似于虚拟机。对于那些记得使用chroot在其他 Linux 发行版上运行软件的人来说,也可以将其描述为强化版的chroot。

我发现systemd-nspawn在以下场景中特别有用:

  1. 当你希望在 VPS 上以某种程度的隔离运行某些软件,而由于嵌套虚拟化不可用1,你无法创建完整的虚拟机时;
  2. 当你需要共享对硬件(如 GPU)的访问权限时(这就是我在systemd-nspawn中运行 LLM 的原因);
  3. 当你不想承担虚拟化的开销时;
  4. 当你希望直接访问宿主机系统上的某些文件,而不必借助virtiofs时;以及
  5. 当你通常会使用 Docker 但无法或不想使用时。原因请见由脚注演变成的博客文章。

在本文中,我将描述设置systemd-nspawn容器的过程,以及如何在一些常见场景中使用它们。

更新(2025-05-19): 添加了更安全的 Alpine 容器创建说明 以及更多关于设置 Alpine Linux 容器的指导。

从chroot开始

systemd-nspawn与所有容器一样,需要在目录中有一个独立的 rootfs。由于它不附带为你构建镜像的镜像管理器,你需要使用传统的 Unix 方法来创建一个chroot。毕竟,它就是强化版的chroot。

对于systemd-nspawn,通常的惯例是将这些 rootfs 放在/var/lib/machines/[hostname]下,其中主机名即容器的名称。在本练习中,我将直接使用操作系统的名称作为主机名。

每个 Linux 发行版都有自己的创建新 rootfs 的方式,因此我们将介绍一些较为常见的方法。我是在 Debian 上测试所有这些的,但没有理由你不能通过替换等效的软件包在其他发行版上完成这些操作。不过,我可能会更深入地介绍 Debian,因为我对它更熟悉。

Debian 系上的debootstrap

debootstrap已多次出现在本博客中,主要是在做一些疯狂的事情时,比如从救援系统手动安装 Debian。对于基于 Debian 的发行版(如 Ubuntu 或 Debian 本身),这是用来创建 rootfs 的工具。

在基于 Debian 的操作系统上,使用sudo apt install debootstrap安装它。它也可能在其他发行版上打包,或者你可以直接从 Debian 的软件仓库下载:

$ curl -L https://salsa.debian.org/installer-team/debootstrap/-/archive/master/debootstrap-master.tar.gz | tar xz
...
$ cd debootstrap-master/
$ ./debootstrap 
I: usage: [OPTION]... <suite> <target> [<mirror> [<script>]]
...

无论你以何种方式安装了debootstrap,让我们在/var/lib/machines/debian中创建 rootfs:

$ sudo debootstrap --include=systemd,dbus,locales,bash-completion,curl bookworm /var/lib/machines/debian
I: Target architecture can be executed
I: Retrieving InRelease 
...
I: Base system installed successfully.

这里,我们已经安装了bookworm,但你可以随意替换为最新的 Debian 版本。为了避免出现愚蠢的问题,我确保安装了systemd和dbus,以及locales以避免在apt中出现奇怪的警告,bash-completion这样我在手动输入命令时就不会讨厌自己的生活,还有curl以便你之后可以下载东西。其中一些需要稍后进行额外配置,我们将在启动进入容器后完成。

使用dnf安装 RHEL 衍生版

首先,如果你使用的是其他发行版,例如 Debian,请安装dnf和rpm:

$ sudo apt install dnf rpm
...
Setting up dnf (4.14.0-3+deb12u1) ...
Setting up rpm (4.18.0+dfsg-1+deb12u1) ...
...
$ sudo mkdir -p /var/lib/rpm
$ sudo rpm --initdb

然后,我们以创建一个 Fedora 41 chroot 为例:

$ sudo mkdir -p /var/lib/machines/fedora/etc/dnf
$ sudo tee /var/lib/machines/fedora/etc/dnf/dnf.conf > /dev/null
[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
$ sudo dnf --releasever=41 --best --setopt=install_weak_deps=False --repo=fedora --repo=updates --installroot=/var/lib/machines/fedora install dnf fedora-release glibc glibc-langpack-en iproute iputils less ncurses passwd systemd systemd-networkd systemd-resolved util-linux vim-default-editor
...
Transaction Summary
================================================================================
Install  137 Packages

Total download size: 62 M
Installed size: 206 M
Is this ok [y/N]: y
...
Complete!

使用alpine-nspawn-install安装 Alpine Linux

Alpine Linux 因其轻量级大小而常用于 Docker 镜像,所以我想不妨尝试创建一个 Alpine Linux chroot。通常用于此类事情的工具有alpine-chroot-install,但其默认行为对我们的用例来说很可怕。例如:

  1. 如果当前目录位于/home内,它会出于“兼容性原因”将其绑定到它配置的 chroot 中,且没有任何警告。当我在 chroot 上运行rm -rf时,我差点意外删除了整个主目录……
  2. 它还会将/dev、/proc和/sys挂载到 chroot 中。对于/dev,任何后续尝试chown容器以利用 UID 隔离的操作也会chown主机系统上的所有设备,导致一场灾难,幸运的是可以通过重启来修复。

之后可以运行一个疯狂的umount命令来清理alpine-chroot-install留下的东西,例如:

mount | grep /var/lib/machines/alpine | cut -d' ' -f3 | sort -r | while read -r path; do sudo umount "$path"; done

然而,我认为这是一个定时炸弹,随时可能在某人面前爆炸,因此,我分叉了脚本并创建了alpine-nspawn-install。我的版本移除了alpine-chroot-install中所有对systemd-nspawn不合理的疯狂“特性”,例如挂载、chroot 辅助脚本、qemu支持等。2,同时默认安装更合理的软件包列表。你可以这样使用它:

$ wget https://raw.githubusercontent.com/quantum5/alpine-nspawn-install/refs/heads/master/alpine-nspawn-install
...

2025-05-19 22:23:27 (47.6 MB/s) - ‘alpine-nspawn-install’ saved [15897/15897]
$ sudo bash alpine-nspawn-install -d /var/lib/machines/alpine
...
---
Alpine container installation is complete.

现在让我们看看 Alpine 是否名副其实:

$ sudo du -sh /var/lib/machines/debian
338M	/var/lib/machines/debian
$ sudo du -sh /var/lib/machines/fedora
507M	/var/lib/machines/fedora
$ sudo du -sh /var/lib/machines/alpine
16M	/var/lib/machines/alpine

嗯,这确实令人印象深刻…… Alpine 比其他两个小一个数量级,而 Fedora 在我安装的软件包列表下相当臃肿。我不是 Fedora 用户,是从Arch Linux wiki 上关于systemd-nspawn的指南复制了软件包列表,所以我不确定是不是因为我安装了臃肿的软件,但它们似乎都很有必要……它肯定比dnf声称的要大,所以我看了看,dnf在/var/cache/dnf下创建了 260 MiB 的东西。

另外,为了对 Debian 公平,我确实安装了一堆“臃肿”的东西,比如语言环境,以获得更愉快的体验。移除“臃肿”后结果为 305 MiB,稍微小一点。

使用systemd-nspawn进行初始启动

首先,使用sudo apt install systemd-container或你的发行版等效工具安装systemd-nspawn。

然后,你可以通过运行以下命令在容器中获得一个 root shell:

$ sudo systemd-nspawn -D /var/lib/machines/[name] -M [name] --private-users=pick --private-users-ownership=chown

以下是每个参数的作用:

  • -D(--directory)指定 rootfs 的目录;
  • -M(--machine)指定机器名称,该名称也设置为容器内的主机名;
  • --private-users=pick 通过从宿主机上未使用的区块中选取 65536 个 UID 和 GID 来启用 UID 和 GID 命名空间,起始值为 524288 到 1878982656 范围内 65536 的倍数3。其他选项包括 yes,它从 rootfs 的所属 UID 和 GID 开始选取范围(必须是 65536 的倍数);no,禁用 UID 命名空间;一个整数,作为 65536 个 UID 区块的起始 UID;或格式为 a:b 的字符串,其中 a 是起始 UID 以及要分配给容器的 UID 数量;最后
  • --private-users-ownership=chown 将 rootfs 的所有权更改为所选 UID 和 GID 的所有权,以便在未来的启动中,--private-users=yes 将继续使用相同的 UID 和 GID。

以 Debian 为例进行启动:

$ sudo systemd-nspawn -D /var/lib/machines/debian -M debian --private-users=pick --private-users-ownership=chown
Spawning container debian on /var/lib/machines/debian.
Press ^] three times within 1s to kill container.
Selected user namespace base 1766326272 and range 65536.
root@debian:~#

现在我们已进入新创建的 Debian 容器。请注意,这仅仅是一个 root shell,并不是长期部署容器的正确方式。甚至没有 init 进程。

Debian 特定的设置提示

如果你打算长时间使用 root shell,并希望自动补全以避免输入完整命令:

root@debian:~# cp /etc/skel/.bashrc .
root@debian:~# . ~/.bashrc

如果你只是使用容器运行单个应用程序,管理自己的用户然后再 sudo 会非常繁琐,所以我选择以 root 身份进行管理,拥有一个正常工作的 .bashrc 效果奇佳。这在技术上并非最佳实践,但我基本上从不在单个应用程序容器中运行不需要 root 的命令,所以实际上没什么区别。

debootstrap 还将宿主机的 hostname 复制到了 /etc/hostname 中。让我们修复这个问题:

root@debian:~# hostname > /etc/hostname

重新进入容器

如果你因某种原因退出了 root shell,可以通过以下方式返回:

$ sudo systemd-nspawn -D /var/lib/machines/debian -M debian --private-users=yes
Spawning container debian on /var/lib/machines/debian.
Press ^] three times within 1s to kill container.
Selected user namespace base 1766326272 and range 65536.
root@debian:~#

准备容器

像这样运行容器并非长期解决方案。你可能希望将其设置为在后台正常运行。然而,这留下了如何访问容器的问题。如果你使用的是基于 systemd 的发行版,你有几个选项:

  • 如果你只想从宿主机访问容器,你不需要做任何事情,因为你会看到如何通过 machinectl 获得直接的 shell 访问。你还可以:
  • Set a root password with passwd so you can log in from a terminal:
    root@debian:~# passwd
    New password:
    Retype new password:
    passwd: password updated successfully
    
  • Install openssh-server and set up SSH keys to connect remotely (you’ll also need to set up networking, as explained later, or you won’t be able to connect):
    root@debian:~# apt install openssh-server
    ...
    Created symlink /etc/systemd/system/sshd.service → /lib/systemd/system/ssh.service.
    Created symlink /etc/systemd/system/multi-user.target.wants/ssh.service → /lib/systemd/system/ssh.service.
    Processing triggers for libc-bin (2.36-9+deb12u10) ...
    root@debian:~# curl -q https://ssh.qt.ax -o ~/.ssh/authorized_keys
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
    100   243  100   243    0     0   3934      0 --:--:-- --:--:-- --:--:--  3983
    

    请注意,因为我们现在只有一个 root shell,而不是正常启动,systemd(或任何其他 init 系统)实际上并未运行——所以暂时不要期望能够启动 sshd。下一步将正确启动容器。

  • 创建一个用户,设置 sudo 等,并以该用户身份登录。这留给读者作为练习。

请注意,通过 machinectl 进行控制台和 shell 访问需要 systemd 在容器内运行。在使用非 systemd 发行版(如 Alpine)时,这将无法实现。如果你使用的是 Alpine,你需要 openssh-server 路线。

Alpine Linux 注意事项

由 alpine-chroot-install 创建的默认 Alpine Linux 安装仅配置用于 chroot 使用,甚至不包括一个正常工作的 init 系统。要在 systemd-nspawn 容器中正确运行它,我们需要安装 openrc(它们的 init 系统)以及 openssh-server:

alpine:~# apk add openrc openssh-server
...
alpine:~# rc-update add sshd
alpine:~# rc-update add networking

如果你改用我的 alpine-nspawn-install 脚本,这一切都已为你完成。你只需进行一些基本配置:

alpine:~# hostname > /etc/hostname
alpine:~# mkdir -p ~/.ssh
alpine:~# wget https://ssh.qt.ax -O ~/.ssh/authorized_keys
...

Alpine 使用 ifupdown-ng 来配置网络,其语法应与 Debian 的兼容,因此你可以直接复制下面的示例。你需要在启动容器之前完成网络配置,否则你将无法通过 ssh 登录。

使用 machinectl

我们可以创建 /etc/systemd/nspawn/[name].nspawn 来管理容器,使用 machinectl,例如 machinectl start [name]。请注意, /etc/systemd/nspawn 目录默认不会被创建,因此你可能需要:

$ sudo mkdir -p /etc/systemd/nspawn

.nspawn 文件是一个 INI 格式的文本文件,其中包含与 systemd-nspawn 的命令行参数等效的内容。对于基础部分,执行 --private-users=yes 的等效操作:

[Exec]
PrivateUsers=yes

还有许多其他选项,例如网络、资源限制,以及将宿主机系统上的目录绑定挂载到容器中。我们稍后会介绍这些。

对于像 Alpine 这样的非 systemd 发行版,将 KillSignal=SIGTERM 添加到 [Exec] 部分,以确保容器停止时 init 能够正常终止。SIGRTMIN+3 的默认信号只有 systemd 能理解,而 SIGTERM 被更广泛地理解。

现在,让我们尝试在后台启动容器,使用完整的 init 系统:

$ sudo tee /etc/systemd/nspawn/debian.nspawn > /dev/null
[Exec]
PrivateUsers=yes
$ sudo machinectl start debian
$ sudo machinectl login debian
Connected to machine debian. Press ^] three times within 1s to exit session.

Debian GNU/Linux 12 debian pts/1

debian login: root
Password: 
Linux debian 6.1.0-32-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.129-1 (2025-03-06) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed Mar 19 22:59:57 EDT 2025 on pts/1
root@debian:~#

我们已经登录了!要退出,请按三次 Ctrl+] 从控制台断开连接,正如 machinectl 所提示的那样。

你也可以直接用 machinectl shell 获得一个 shell:

$ sudo machinectl shell root@debian
Connected to machine debian. Press ^] three times within 1s to exit session.
root@debian:~#

要停止这个容器,请使用 sudo machinectl stop。对于非 systemd 发行版,请使用 sudo systemctl stop systemd-nspawn@[name]。

要在启动时运行此容器,你只需运行:

$ sudo machinectl enable debian

要撤销此操作,请使用 sudo machinectl disable。

请注意,machinectl 只是控制底层的 systemd 单元, systemd-nspawn@[name].service。这在后面会变得很重要。请注意,你可以通过运行 sudo systemd edit systemd-nspawn@[name].service 来为单元配置额外的选项。

网络

网络在 .nspawn 文件的 [Network] 部分下进行配置。 有几个选项:

使用主机网络

这意味着容器与宿主机共享同一网络。任何地方都没有网络隔离。这是最容易设置的选项,但隔离性不太好,所以如果你想要隔离,我不推荐它。

这是直接调用 systemd-nspawn 时的默认行为,这就是为什么之前 apt install 在没有任何网络配置的情况下也能工作。可以通过在 .nspawn 中使用 VirtualEthernet=no 来启用它。

使用网桥

Example bridge topology for containers
容器的网桥拓扑示例

Linux 网桥基本上就是一个虚拟以太网交换机。在 Debian 上,你可以通过安装 bridge-utils 软件包并修改你的 /etc/network/interfaces,使用 ifupdown 创建一个。

如果你在家里运行一台位于路由器后面的 PC(或者以某种方式从托管服务商那里获得了大量 IP),你可以将你当前的接口(我们以 eth0 为例)添加到一个网桥。这就像把连接到 eth0 的网线和你的 PC 都连接到一个虚拟交换机,以及任何容器。 你需要将当前在 eth0 上的 IP 分配复制到网桥上,因为它实际上将 eth0 从你的机器中拉了出来。你的 IP 配置 将需要在网桥接口上完成,该接口连接到交换机上为你的机器准备的虚拟端口。

对于静态 IP,将以下配置段添加到 /etc/network/interfaces,并 删除(或用 # 注释掉)eth0 的块:

auto br_test
iface br_test inet static
    bridge-ports eth0
    bridge-stp off
    bridge-fd 0
    address 192.0.2.123/24 # replace this with the current eth0 IPv4 address
    gateway 192.0.2.1      # replace this with the current eth0 IPv4 gateway

# remove this whole section if you don't have IPv6
iface br_test inet6 static
    address 2001:db8::123/64 # replace this with the current eth0 IPv6 address
    gateway 2001:db8::1      # replace this with the current eth0 IPv6 gateway

或者,如果你更喜欢 IPv4 使用 DHCP,IPv6 使用 SLAAC:

auto br_test
iface br_test inet dhcp
    bridge-ports eth0
    bridge-stp off
    bridge-fd 0

iface br_test inet6 auto

你当然可以随意命名网桥,不过建议名称以 br 开头。另请注意,这会关闭 生成树协议(STP)以使新连接更快上线,但无论如何你都不应该让这个虚拟网桥参与环路拓扑。在其他平台或网络工具上做网桥留给读者作为练习。Arch Linux wiki 在这里可能会有所帮助,即使你没有使用 Arch Linux。

然后,你只需在 .nspawn 文件中使用 Bridge=br_test (命令行上使用 --network-bridge=br_test)将容器内的虚拟接口 host0 连接到网桥。然后你可以在容器内设置 /etc/network/interfaces(或你的发行版的等效项),如下所示:

auto host0
iface host0 inet static
    address 192.0.2.124/24 # replace this with an unused IP address on the network
    gateway 192.0.2.1      # replace this with the current eth0 IPv4 gateway

# remove this whole section if you don't have IPv6
iface host0 inet6 static
    address 2001:db8::123/64 # replace this with an unused IP address on the network
    gateway 2001:db8::1      # replace this with the current eth0 IPv6 gateway

或者,如果你更倾向于 IPv4 使用 DHCP、IPv6 使用 SLAAC:

auto host0
iface host0 inet dhcp
iface host0 inet6 auto

请注意,你可以将多个容器连接到同一个网桥,因此你只需执行一次此操作。

如果你在客户机中使用 systemd-networkd,请在容器中创建包含以下内容的 /etc/systemd/network/host0.network:

[Match]
Name=host0

[Link]
RequiredForOnline=routable

[Network]
# for DHCP
DHCP=yes
# or comment it out and use static IPs:
Address=192.0.2.124/24
Gateway=192.0.2.1
Address=2001:db8::123/64
Gateway=2001:db8::1

你可能需要启用 systemd-networkd:

[root@fedora ~]# systemctl enable --now systemd-networkd

使用 NAT

Example NAT topology for containers
容器的 NAT 拓扑示例

或者,如果你无法桥接当前网络(例如你使用的是 VPS,且只分配了一个 IP),也并非无计可施。你可以创建带有自定义网络的网桥,并改为通过它进行路由。这个网桥是新的虚拟网络,真实的网络接口并未接入其中。

对于 IPv4,我建议你从 RFC 1918 范围内随机为自己分配一个 /24。我们将以 192.0.2.0/24 为例,因为它保留用于文档。

对于 IPv6,请向你的 ISP 申请委派一个 /64(或更大的块,但此用途你只需要 /64),大多数有能力的4 VPS 提供商应该都能做到。如果你是在家中运行,请看看你的路由器能否使用 DHCP-PD 请求一个大块,并向主机委派一个 /64。我们将以 2001:db8::/64 为例。

无论哪种情况,按照惯例将子网中的第一个地址分配给网桥。

auto br_test
iface br_test inet static
    bridge-ports none
    bridge-stp off
    bridge-fd 0
    address 192.0.2.1/24

# remove this if you can't get a IPv6 block
iface br_test inet6 static
    address 2001:db8::1/64

然后,你需要通过运行以下 iptables 命令来设置 NAT:

$ sudo iptables -t nat -A POSTROUTING -s 192.0.2.0/24 -j MASQUERADE

或者,如果主机上有静态 IP,比如 198.51.100.123,你也可以使用 SNAT 规则,这样更快:

$ sudo iptables -t nat -A POSTROUTING -s 192.0.2.0/24 -j SNAT --to-source 198.51.100.123

你需要在每次启动时运行此命令。具体做法留给读者作为练习。你可以使用某个预构建防火墙、一个 systemd 单元,甚至 rc.local。更多细节,请查阅你的发行版关于 iptables 的文档。

你还需要配置以下 sysctl。你可以将它们添加到 /etc/sysctl.conf 中,或取消注释已存在的那一行:

net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1

然后运行 sudo sysctl -p。这将允许主机充当路由器。

完成之后,像之前一样在你的 .nspawn 文件中使用 Bridge=br_test,将容器内的 host0 连接到网桥。然后你可以在容器内(或你的发行版中的等效项)按如下方式设置 /etc/network/interfaces:

auto host0
iface host0 inet static
    address 192.0.2.2/24 # replace this with an address not already in use 
    gateway 192.0.2.1

# remove this whole section if you don't have IPv6
iface host0 inet6 static
    address 2001:db8::2/64 # replace this with an address not already in use
    gateway 2001:db8::1

对于 systemd-networkd,使用与上面网桥部分相同的示例。

你也可以使用 DHCP 和 SLAAC,但这需要在主机上运行 DHCP 和路由通告服务器。可选方案包括 dnsmasq。具体做法留给读者作为练习。

请注意,你可以通过 NAT 将多个容器连接到同一个网桥,因此你只需执行一次此操作。

连接到容器

现在网络已配置好,你应该能够连接到容器了。例如,使用 ssh 和网桥示例:

$ ping 192.0.2.124
PING 192.0.2.124 (192.0.2.124) 56(84) bytes of data.
64 bytes from 192.0.2.124: icmp_seq=1 ttl=64 time=0.141 ms
^C
--- 192.0.2.124 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.141/0.141/0.141/0.000 ms
$ ssh root@192.0.2.124
...
root@debian:~# 

从主机系统绑定文件和目录

另一件常见的事情是将主机上的某些文件和目录挂载到容器中。这可以在 .nspawn 文件的 [Files] 部分完成。以下是一些示例:

[Files]
# Mounts /srv/example on the host with read/write access into the container
# at the same path
# Equivalent to --bind=/srv/example on the command line
Bind=/srv/example

# Mounts /srv/example on the host with read-only access at /data
# inside the container
# Equivalent to --bind-ro=/srv/example:/data on the command line
BindReadOnly=/srv/example:/data

# Mounts /srv/example on the host with read/write access into the container
# at /test, but without recursively binding anything mounted underneath
Bind=/srv/example:/test:norbind

第三个参数允许指定逗号分隔的绑定选项。有两个与绑定挂载风格相关:

  • rbind(默认):递归绑定挂载主机路径下的内容;以及
  • norbind:不要在主机路径下绑定挂载。

还有四个与 UID 映射相关的选项。回想一下,容器在主机上被赋予 y 个 UID,范围为 [x,x+y)。这些选项是:

  • noidmap(默认):将容器内的 UID z∈[0,y) 映射到主机上的 UID x+z,超出范围的 UID 在客户机内显示为 nobody5;
  • idmap:执行身份映射,即容器内的 UID z∈[0,y) 将映射为主机上的 UID z,超出范围的 UID 显示为 nobody;
  • rootidmap:容器内的 root 用户映射为被绑定路径的所有者,主机上的其他所有用户将显示为 nobody;以及
  • owneridmap:容器内挂载点的所有者映射为被绑定路径的所有者,主机上的其他所有用户将 显示为 nobody。

你可以非常轻松地使用 noidmap,通过授予主机上 x+z 的访问权限,来允许容器内用户 z 的访问。你可以用传统权限来实现,但我比较懒,就直接用 POSIX ACL 了,它灵活得多。例如,要授予 UID 13333337 对 /srv/example 下所有内容的写权限,你只需运行(在 Debian 上安装 acl 包或等效操作后):

$ setfacl -Rm u:13333337:rwX /srv/example

如果你还想让这一设置应用于该目录下新建的文件,还可以运行:

$ setfacl -Rdm u:13333337:rwX /srv/example

也许有一天我会写一篇 POSIX ACL 的入门介绍,但网上有大量资源,目前细节的探究就留给读者作为练习。

另请注意,你不一定非要绑定挂载目录。你也可以绑定挂载文件。不用说,不要给容器授予超出必要的访问权限。

共享 GPU

另一个有趣的用途是与客户机共享主机的 GPU。请注意,与在 KVM 虚拟机上做 VFIO 直通不同,该设备可以在主机和多个客户机之间共享。这是通过绑定挂载设备文件实现的。

Nvidia GPU

在 .nspawn 文件中:

[Files]
Bind=/dev/nvidia0
Bind=/dev/nvidiactl
Bind=/dev/nvidia-modeset
Bind=/dev/nvidia-uvm
Bind=/dev/nvidia-uvm-tools

你需要确保客户机内的驱动版本与主机上的匹配。你还需要通过 systemctl edit systemd-nspawn@[name].service 允许该 systemd 单元访问,并加入以下 行:

[Service]
DeviceAllow=/dev/nvidia0 rw
DeviceAllow=/dev/nvidiactl rw
DeviceAllow=/dev/nvidia-modeset rw
DeviceAllow=/dev/nvidia-uvm rw
DeviceAllow=/dev/nvidia-uvm-tools rw

Intel 和 AMD GPU

在 .nspawn 文件中:

[Files]
Bind=/dev/dri

请注意,这将共享所有 GPU 的访问权限。直通特定 GPU 留给读者作为练习。

限制资源使用

你可以像这样设置 CPU 亲和性来限制 CPU 核心:

[Exec]
CPUAffinity=0-1,8-9

你还可以通过标准的 systemd 机制编辑该单元来添加 CPU 和内存限制,例如:

[Service]
# Note that a CPU quota > 100% means allowing more than one core,
# so this means two full cores are allowed:
CPUQuota=200%

MemoryMax=1G
MemorySwapMax=512M

# Limit I/O to 100 MB/s
IOReadBandwidthMax=100M
IOWriteBandwidthMax=100M

更多详情,请参阅 systemd 资源控制文档。

结论

以上就是对 systemd-nspawn 的一次快速巡览。我们讲了很多,但这主要是因为设置方式有太多合理的选择。借助 systemd-nspawn,你可以自由地以自己喜欢的方式配置一切,拥有完全的控制权。归根结底,它就像一台虚拟机。你想做什么都可以!

一旦你找到了自己喜欢的设置,你其实只需要一个名称和一个 IP 地址分配,然后在首次启动时运行几条命令来准备镜像。其余的一切只需编辑 .nspawn 文件。你甚至可以自己编写脚本来自动化一切。当然,这样做就留给读者作为练习了。

备注

  1. 即使嵌套虚拟化可用,它通常也有奇怪的限制和稳定性问题。通常不建议在生产环境中运行。 ↩

  2. 其他被删除的疯狂功能包括:将运行 sudo 的用户复制到 chroot 的方式,或随机调用 apt 在主机系统上安装软件包的方式,或它在容器内乱搞 sudo 规则的方式。 ↩

  3. 对于习惯用二进制思考的人来说,这实际上就是把容器内 UID/GID 的低 16 位拿来用,并把高 16 位设为 0x8 到 0x6fff 之间的某个值。 ↩

  4. 那些至少不给你分配一个 /64(无论是默认还是按需)供你自己使用的 VPS 提供商,就是没有正确部署 IPv6,应该为此感到羞愧。对不了解的人来说,IPv6 特意不使用 NAT,而是为网络上的所有主机分配一个全球唯一的 IP 地址。因此,并且为了方便 SLAAC,每个 L2 网络都需要分配一个 /64 块。好的 ISP 知道这一点,会按需分配这样的块,甚至发放 /56 或 /48 这样更大的块,这样你就不必总是去麻烦他们。 ↩

  5. 虽然无法映射的 UID 在容器内显示为 nobody,但这并不意味着 nobody 用户真的能访问那些文件。访问权限检查的是宿主机所看到的真实 UID。请记住,root 映射到宿主机上的 UID x。如果 UID x 没有访问权限,那么即使容器内的 root 用户也会被拒绝访问。 ↩

来源:Hacker News · quantum5.ca