在 Void Linux 上用 Turnstile 运行 Rootless Docker
Docker 官方的 Rootless 安装方式 默认生成 systemd 用户服务,而 Void Linux 使用 runit。Void 仓库中的 moby 包提供了 dockerd,却没有附带 dockerd-rootless.sh 和 dockerd-rootless-setuptool.sh。
所以我的做法是手动安装 Docker、RootlessKit 和 pasta,再把启动逻辑写成 Turnstile 管理的 runit 用户服务。
安装依赖
sudo xbps-install -S docker rootlesskit passt turnstile
Turnstile 的安装和用户服务用法直接参考 Void Handbook: Per-User Services。我使用默认的 runit backend,只有 /etc/turnstile/turnstiled.conf 中这一项改为:
manage_rundir = no
用户服务仍放在默认的 ~/.config/service/。
编写 Docker 用户服务
创建服务目录:
mkdir -p ~/.config/service/docker
目录中有三个文件:
~/.config/service/docker/
├── run # runit 入口
├── conf # 本机可调参数
└── dockerd-rootless-shim # RootlessKit 启动包装脚本run
run 只做三件事:合并标准错误、加载 conf、把进程替换为包装脚本。
#!/bin/sh
exec 2>&1
service_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1
# 加载用户配置
if [ -r "$service_dir/conf" ]; then
. "$service_dir/conf"
fi
# OPTS 有意进行 shell 参数拆分
exec "$service_dir/dockerd-rootless-shim" ${OPTS:-}conf
我选择 pasta 网络,并配合 RootlessKit 推荐的 implicit 端口驱动:
#!/bin/sh
# 强制使用 pasta 网络驱动
export DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta
# pasta 推荐使用 implicit 端口驱动
export DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit
# pasta 默认 MTU
export DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=65520
# 使用分离的网络命名空间
export DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=true
# 允许容器通过 10.0.2.2 访问宿主机回环地址
#
# true:
# 不向 RootlessKit 传递 --disable-host-loopback
# pasta 不会启用 --no-map-gw
#
# false:
# 禁止容器访问宿主机 127.0.0.1 上的服务
export DOCKERD_ROOTLESS_ALLOW_HOST_LOOPBACK=true
# 让下面的容器参数:
#
# --add-host=host.docker.internal:host-gateway
#
# 映射到 pasta 的宿主机网关 10.0.2.2
OPTS="${OPTS:+$OPTS }--host-gateway-ip=10.0.2.2"
export OPTS
pasta 需要配合 --port-driver=implicit 使用,具体见 RootlessKit network drivers。这里同时允许容器通过 10.0.2.2 访问宿主机的回环地址。
只绑定到 127.0.0.1 时的坑
后来我发现,用 -p 127.0.0.1:8080:8080 这种形式转发端口时,宿主机反而无法访问 127.0.0.1:8080。排查之后才发现,不是 Docker 的端口映射有问题,而是 Void 仓库里的 RootlessKit 版本落后了:目前仍是 2.2.0,启动 pasta 时不会传入 --host-lo-to-ns-lo。
上游从 RootlessKit 2.3.2 开始会检测 pasta 是否支持这个参数,并在启动 pasta 时自动加上它。升级 RootlessKit 后,绑定到宿主机回环地址的端口就能正常访问了。
唉,Void。Void Linux 是我很喜欢的一个发行版,但无奈维护人手太少,一些包总是落后,另一些则迟迟无人打包。这大概也是使用小众发行版时不得不接受的一部分。
dockerd-rootless-shim
Void 没有提供官方的 dockerd-rootless.sh,所以下面的脚本负责完成等价的 RootlessKit 启动工作,并加入 pasta 和 Docker bridge 所需的处理。
完整的 dockerd-rootless-shim(点击展开)
#!/bin/sh
set -eu
# ============================================================
# RootlessKit 子进程
# ============================================================
if [ -n "${_DOCKERD_ROOTLESS_CHILD:-}" ]; then
# /run 已由 RootlessKit copy-up,在子命名空间中清理旧路径
rm -f /run/docker /run/containerd /run/xtables.lock
# 默认使用当前进程的网络命名空间。
# 启用 --detach-netns 时优先使用 RootlessKit 保存的 netns。
netns="/proc/self/ns/net"
if [ -n "${ROOTLESSKIT_STATE_DIR:-}" ] &&
[ -e "$ROOTLESSKIT_STATE_DIR/netns" ]; then
netns="$ROOTLESSKIT_STATE_DIR/netns"
fi
# Docker bridge 和 nftables 需要网络命名空间开启转发
nsenter --net="$netns" -- \
sysctl -w net.ipv4.ip_forward=1 >/dev/null
nsenter --net="$netns" -- \
sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true
exec dockerd "$@"
fi
# ============================================================
# RootlessKit 父进程
# ============================================================
if [ "$(id -u)" = "0" ]; then
echo "错误:不能以 root 身份运行 rootless Docker" >&2
exit 1
fi
if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
echo "错误:XDG_RUNTIME_DIR 未设置" >&2
exit 1
fi
if [ ! -d "$XDG_RUNTIME_DIR" ] ||
[ ! -w "$XDG_RUNTIME_DIR" ]; then
echo "错误:XDG_RUNTIME_DIR 不存在或不可写:$XDG_RUNTIME_DIR" >&2
exit 1
fi
if [ -z "${HOME:-}" ] || [ ! -d "$HOME" ]; then
echo "错误:HOME 未设置或目录不存在" >&2
exit 1
fi
# ============================================================
# 查找 RootlessKit
# ============================================================
rootlesskit_bin=""
for candidate in docker-rootlesskit rootlesskit; do
if command -v "$candidate" >/dev/null 2>&1; then
rootlesskit_bin="$candidate"
break
fi
done
if [ -z "$rootlesskit_bin" ]; then
echo "错误:找不到 docker-rootlesskit 或 rootlesskit" >&2
exit 1
fi
# ============================================================
# pasta 配置
# ============================================================
net="${DOCKERD_ROOTLESS_ROOTLESSKIT_NET:-pasta}"
if [ "$net" = "pasta" ] &&
! command -v pasta >/dev/null 2>&1; then
echo "错误:配置了 pasta 网络,但系统中找不到 pasta 命令" >&2
exit 1
fi
mtu="${DOCKERD_ROOTLESS_ROOTLESSKIT_MTU:-65520}"
if [ -n "${DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER:-}" ]; then
port_driver="$DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER"
elif [ "$net" = "pasta" ]; then
port_driver="implicit"
else
port_driver="builtin"
fi
# RootlessKit 官方建议 pasta 与 implicit 端口驱动配合使用
if [ "$net" = "pasta" ] && [ "$port_driver" != "implicit" ]; then
echo "警告:pasta 通常应配合 --port-driver=implicit" >&2
fi
# ============================================================
# detach-netns
# ============================================================
detach_flag=""
case "${DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS:-true}" in
1|true|yes|on)
detach_flag="--detach-netns"
;;
0|false|no|off)
detach_flag=""
;;
*)
echo "错误:无效的 DETACH_NETNS 值" >&2
exit 1
;;
esac
# ============================================================
# 宿主机回环访问
# ============================================================
host_loopback_flag=""
case "${DOCKERD_ROOTLESS_ALLOW_HOST_LOOPBACK:-false}" in
1|true|yes|on)
# 不传 --disable-host-loopback。
#
# 对 pasta 而言,这意味着 RootlessKit 不会给 pasta 添加:
#
# --no-map-gw
#
# 容器可通过 10.0.2.2 访问宿主机 127.0.0.1 服务。
host_loopback_flag=""
;;
0|false|no|off)
host_loopback_flag="--disable-host-loopback"
;;
*)
echo "错误:无效的 ALLOW_HOST_LOOPBACK 值" >&2
exit 1
;;
esac
# ============================================================
# 启动 RootlessKit
# ============================================================
state_dir="$XDG_RUNTIME_DIR/dockerd-rootless"
mkdir -p "$state_dir"
export _DOCKERD_ROOTLESS_CHILD=1
exec "$rootlesskit_bin" \
--state-dir="$state_dir" \
--net="$net" \
--mtu="$mtu" \
--port-driver="$port_driver" \
--copy-up=/etc \
--copy-up=/run \
--propagation=rslave \
${host_loopback_flag:+$host_loopback_flag} \
${detach_flag:+$detach_flag} \
"$0" "$@"脚本会执行两次:父进程检查环境并启动 RootlessKit,子进程在 namespace 内设置网络转发并执行 dockerd。/etc 和 /run 使用 copy-up,避免与宿主机的运行文件和 DNS 配置互相影响。
设置权限:
chmod 755 ~/.config/service/docker/run
chmod 755 ~/.config/service/docker/dockerd-rootless-shim
chmod 600 ~/.config/service/docker/conf
Turnstile 的 runsvdir 会自动发现新目录。服务可以直接用路径控制:
sv up ~/.config/service/docker
sv status ~/.config/service/docker
sv restart ~/.config/service/docker让 Docker CLI 连接 rootless socket
Rootless daemon 默认监听 $XDG_RUNTIME_DIR/docker.sock。让客户端连接这个 socket:
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock"使用 NVIDIA Container Toolkit
如果要在容器中使用 NVIDIA GPU,安装 NVIDIA Container Toolkit 后,配置 Docker runtime 并启用 CDI:
sudo nvidia-ctk runtime configure --runtime=docker --cdi.enabled
然后在 /etc/rc.local 中加入以下内容,让系统启动时生成 NVIDIA CDI 配置,并为 Rootless Docker 设置共享挂载:
echo "[-] Generating NVIDIA CDI..."
if nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml >/dev/null 2>&1; then
echo "[*] NVIDIA CDI ready."
else
echo "[!] NVIDIA CDI generation failed."
fi
echo "[-] Setting up shared mounts for Rootless Docker..."
mount_failed=0
for mnt in / /dev /sys /proc /tmp; do
if ! mount --make-rshared "$mnt" >/dev/null 2>&1; then
echo "[!] Failed to set rshared on $mnt"
mount_failed=1
fi
done
if [ "$mount_failed" -eq 0 ]; then
echo "[*] Shared mounts ready."
fi验证
docker version
docker info
docker run --rm hello-world
docker version 的 Server 部分应显示:
rootlesskit:
NetworkDriver: pasta
StateDir: /run/user/1000/dockerd-rootless
docker info 应显示:
Security Options:
rootless
Docker Root Dir: /home/你的用户名/.local/share/docker
不需要使用 sudo docker ...。
从容器访问宿主机回环服务
因为本文在 conf 中启用了 host loopback,并把 Docker 的 host-gateway 固定为 pasta 网关 10.0.2.2,可以在 Compose 中这样写:
services:
app:
image: example/app
extra_hosts:
- "host.docker.internal:host-gateway"
容器中的 host.docker.internal 会解析为 10.0.2.2。访问这个地址,相当于访问宿主机的 127.0.0.1 服务。
cgroup
当前方案会看到:
Cgroup Driver: none
WARNING: Running in rootless-mode without cgroups.
在没有 systemd 用户服务的 Void/runit 上,rootless Docker 不支持 cgroup 资源限制,--memory、--cpus 和 --pids-limit 等参数会被忽略。详见 Docker Rootless tips。