Nspawn 3.0:把 Docker Engine API 装到宿主,代价是 manage 等于 root
项目地址:nspawn.org;发布说明:Nspawn 3.0: A Docker-compatible Engine API for systemd-nspawn containers;软件包:apt.nspawn.org、rpm.nspawn.org。
2.0.0 给挂载 docker socket 的机器一个只读 Docker API,够 Traefik 或 Dozzle 发现容器;3.0.0 把这个 API 连同写操作做到宿主上。中间的版本:
- 2.1.0:发布的端口也走 IPv6,
-p接受 IPv6 地址。 - 2.2.0:修整机 compose 项目迁到 nspawn 时暴露的问题。compose 支持匿名卷、定义变化时重建网络、
down SERVICE,以及start会拉起服务依赖。 - 2.3.0:每一层都在它被解出的树里应用,并按镜像为它命名的 digest 校验。
- 2.3.1:包发到 apt.nspawn.org(Debian 13+、Ubuntu 24.04+)和 rpm.nspawn.org(Fedora 44+)。
- 2.4.0:
-p PORT取宿主空闲端口;拉取镜像索引时按宿主平台挑条目。
宿主上的 Docker Engine API
宿主 socket 是 /run/nspawn/api.sock,由 nspawn-api.socket 打开。主要目的是兼容性:通过 Docker API 管容器的工具(比如 GitLab Runner 的 docker executor)可以用 nspawn 的机器。nspawn 仍是管理机器的命令行,但 docker 命令也能对着这个 socket 用。
包默认不启用这个 socket。一旦监听,nspawn-api.service 在第一次连接时启动:
$ sudo systemctl enable --now nspawn-api.socket
把 DOCKER_HOST 指过去,docker 命令就与 nspawn 对话:
$ export DOCKER_HOST=unix:///run/nspawn/api.sock
$ docker run --rm -it alpine sh
$ docker run -d --name web -p 8080:80 nginx
$ docker exec -it web sh
$ docker cp web:/etc/nginx/nginx.conf .
$ docker rm -f web
API server 和命令行一样是总线服务的客户端。它做的每件事都是 org.nspawn 的方法,走同样的 polkit 动作:读和等待需要 org.nspawn.inspect,日志与变更需要 org.nspawn.manage。任何用户都能连,但 polkit 从不会弹认证,因为 socket 上没有东西能应答认证代理。root 能做一切,其他用户需要 polkit 规则,包里有 nspawn-wheel.rules 作为示例。
和 docker 组一样,拥有 org.nspawn.manage 的用户在宿主上就是 root:机器以 root 运行命令,还能挂载任意宿主路径。这是使用这个 socket 的主要安全代价。
单个 socket 的客户端能同时持有的东西(连接、日志与事件流、wait、attach、exec)有上限,非 root 用户各拿一份份额,这样一个用户占不满、也挤不掉别人和 root。
只看 /run/docker.sock 的客户端,在没有别的服务占用该路径的宿主上可以用 drop-in 找到它:
$ sudo mkdir -p /etc/systemd/system/nspawn-api.socket.d
$ printf '[Socket]\nSymlinks=/run/docker.sock\n' | sudo tee /etc/systemd/system/nspawn-api.socket.d/symlink.conf
$ sudo systemctl daemon-reload && sudo systemctl restart nspawn-api.socket
API 提供什么
按 API 1.51 的形状应答,也向下兼容客户端要求的到 1.24 的版本。
- 容器:create、start、stop、restart、kill、pause、unpause、wait、update、remove、prune,带
AutoRemove。create 的设置变成机器的选项:nspawn 用不上的设置从 Warnings 返回,会改变容器行为的设置被 400 拒绝。 - attach:带输入。程序拿到客户端输入,输出逐字节返回,
docker run -i、docker run -it、docker start -ai就靠这个。 - exec:带终端,或 stdout/stderr 分开,支持输入、变量、工作目录、用户。命令不存在退出 127,跑不起来 126。
- archive:
/containers/ID/archive上的 HEAD、GET、PUT,所以docker cp双向可用,运行中和停止的容器都行,保留 owner 和 mode。 - 镜像:pull(带引擎的进度消息和 “Image is up to date”/“Downloaded newer image”)、remove、prune。镜像 ID 是它 config 的 digest。
- 卷与网络:create、inspect、list、remove、prune,以及 connect/disconnect 带 alias 和固定地址,运行与否都行。bridge、host、none 是预定义网络。
GitLab Runner
GitLab Runner 的 docker executor 通过这个 API 在 nspawn 的机器上跑作业。host 设置指向 socket:
[[runners]]
executor = "docker"
[runners.docker]
host = "unix:///run/nspawn/api.sock"
image = "alpine:latest"
变量值现在可以包含换行(-e、compose 的 environment、API 的 Env)。GitLab 的 CI_COMMIT_MESSAGE 就是带正文的那种。
破坏性变更
- 机器内的 API socket 接受写操作。 2.0.0 里机器内的 socket 是只读的。现在以读写挂载
/var/run/docker.sock、/run/docker.sock或/run/nspawn/api.sock的机器可以创建、启动、exec、删除容器,pull/删除镜像,创建卷和网络。挂:ro时只能读、跟日志、kill,其余返回 403。只有机器内的 root 能连,它能看见所有机器和它们的-e变量(--secret值永不出现在其中)。只读的代理或面板应该挂:ro,2.0 文章里的 Traefik 例子就是/var/run/docker.sock:/var/run/docker.sock:ro。 - 拉取或构建出来但从未运行过的记录是镜像,不是容器。 API 的容器列表、inspect、prune 都不含它,
GET /images/json才列它。 - 总线说明失败原因。 失败的方法或作业带
org.nspawn.Error下的错误名:NotAuthorized、NoSuchMachine、NoSuchImage、NoSuchNetwork、NoSuchVolume、NoSuchPath、Exists、InUse、NotRunning、AlreadyRunning、Paused、NotPaused、Invalid,其余为Failed。
其他变更
nspawn info:服务版本、按状态分的机器、镜像、目录、默认后端、日志驱动、registry、宿主。-f json或-f TEMPLATE,服务不应答时退出 1。API 的GET /info由它回答。nspawn images inspect:镜像记录以 JSON 或-f TEMPLATE输出。nspawn image现在与nspawn images是同一组。-P/--publish-all:把镜像暴露、但没有被-p发布的端口发布到宿主空闲端口。- 卷:
volume create --label、volume ls --filter label=、system prune --volumes删除无名卷(只有 API 会创建)。 --timezone auto(默认):给可写 root 一份/etc/localtime的副本而不是 systemd-nspawn 的只读 bind,这样 tzdata 可以替换它。- 未设置的裸
-e VAR被忽略而不是报错,所以-e DEBUG只在设置时传进去。 - 机器输出不会因 journald 限流丢行。unit 的
LogRateLimitIntervalSec=0其实到不了 journald,后者保持默认 30 秒 10000 行。hooks drop-in 现在设置 1 微秒间隔、一百万突发。 - tag 的构建在没有任何东西从它运行时替换该引用的镜像,mkosi 构建也一样。若已有机器或另一个引用的镜像占了这个名字,构建放在旁边用空闲名字(
app-2)并告知。
已知缺口
- 镜像 build、load、import、tag、push、commit,容器 rename 与 export,swarm 和插件,返回 501 “not supported”。对着 socket 跑
docker build不工作,nspawn build可以。 - 停止的容器卷只在运行时挂载,所以对它们的复制在启动前会被拒。
nspawn run -i通过 journal 读输出,会丢空行。API 的 attach 不会。nspawn generate不保留 API 给网络起的名字,compose 拒绝带.的网络名。- mstack 仍是实验性、未测试,不提供 API。
3.1.0 与 3.2.0
两个都已发布:
- 3.1.0 增加
--device-cgroup-rule,用于机器运行期间出现和消失的设备,比如正在刷写的 USB 设备:-v /dev/bus/usb:/dev/bus/usb --device-cgroup-rule 'c 189:* rwm'。拿到宿主设备的应用现在不用用户命名空间跑;跑在用户命名空间里的机器文件 owner 会保持偏移,所以要用nspawn rm再run或create重建。 - 3.2.0 在所有命令里用同样方式读机器或镜像的名字:镜像按本地名或引用,所以
images rm somos/app:1和run somos-app-1都行。run从不消耗或替换镜像:机器建在它旁边。
要求与反馈
要求与 2.0.0 相同。Fedora、Debian/Ubuntu、Arch 的包在 release 页,apt.nspawn.org 与 rpm.nspawn.org 有前两者。如果某个 Docker API 客户端在 docker 上正常、在 nspawn 上失败,带上它发的东西开 issue。