4987 字
25 分钟
Rootless Docker 与 Podman:不用 root 的容器路线
2026-11-15

Docker 用起来很方便,但它有一个从第一天起就被反复讨论的问题:dockerd 守护进程是以 root 身份常驻运行的,容器里的进程默认也离 root 不远。平时自己用没感觉,一旦把服务暴露到公网、跑来路不明的镜像、或者把 docker.sock 挂给了某个容器,风险面就会突然放大。

这一篇走另一条路线:不用 root 跑容器。主线分三段:先讲清为什么要 rootless、它靠什么原理做到;再完整实操一遍 Rootless Docker 的安装和使用限制;最后介绍完全无守护进程的 Podman,并把标准 Docker、Rootless Docker、Podman 三者放在一张表里做选型。安全基础还没补的,建议先看《Docker 安全与资源限制实战》:/posts/docker-security-resources/ ,这一篇是它的进阶延续。

一、为什么要 Rootless#

先把风险说具体,不吓唬人,只摆事实。

标准模式下,Docker 的架构是客户端加守护进程:你敲的每一条 docker 命令,都是发给以 root 运行的 dockerd 去执行的。这带来三个现实问题:

风险点具体含义
守护进程是 rootdockerd 本身或它拉起的组件一旦出漏洞,攻击者直接拿到宿主机 root
容器里的 root 约等于宿主机 root默认没有用户命名空间隔离时,容器内 uid 0 就是宿主机 uid 0,逃逸成功即完全控制
docker.sock 等于 root 权限谁能访问 /var/run/docker.sock,谁就能起一个挂载宿主机根目录的特权容器,把 docker.sock 挂进容器等于把宿主机 root 交出去

所以判断标准很简单:Rootless 的核心价值,是把「容器逃逸」的后果从拿到宿主机 root,降级为只拿到一个普通用户。攻击者就算突破了容器边界,出去之后也只是一个没权限的普通账号,sudo 都未必有。

适合考虑 rootless 的场景:

  • 服务器上要跑第三方镜像、来路不完全可控的服务
  • 多人共用一台机器,每人想自己管自己的容器
  • 对外提供服务,想给安全多加一层兜底
  • 公司合规、安全审计明确要求守护进程不能以 root 运行

代价也要提前说清:rootless 不是免费的,它在端口、网络、资源限制上有若干限制,后文第四节会逐条列并给出解法。

二、原理:用户命名空间与 uid 映射#

Rootless 能成立,靠的是 Linux 的用户命名空间(User Namespace)。

2.1 什么是用户命名空间#

用户命名空间允许一个进程在「自己的小世界」里拥有独立的 uid/gid 视图:进程在命名空间内看自己是 uid 0(root),但在宿主机真实视角里,它只是一个普通用户,比如 uid 1000。

Rootless Docker 启动时,会为当前普通用户创建一个用户命名空间,dockerd 和它管理的所有容器都活在这个命名空间里面。于是:

  • 容器里你看到的 root,是假的 root,只在命名空间内有效
  • 这个假 root 对应到宿主机,是运行 dockerd 的那个普通用户
  • 容器内进程对宿主机文件、进程的访问权限,按普通用户计算,碰不到别人的东西

2.2 subuid 与 subgid:映射的原材料#

问题来了:容器镜像里常常不只有 root 一个用户,还有 uid 1、33、999 等各种系统账号。这些 uid 在命名空间里也要有位置,总不能全挤到同一个宿主机 uid 上。

解决办法是 /etc/subuid 和 /etc/subgid。系统会给每个普通用户分配一段从属 uid 范围,默认每人 65536 个,写在文件里长这样:

anex:100000:65536

含义是:用户 anex 可以使用宿主机 uid 100000 到 165535 这一段,用来映射命名空间内的 uid。映射关系大致如下:

命名空间内 uid宿主机真实 uid说明
0(root)1000运行 dockerd 的用户本人
1100001从 subuid 段顺延
33100033镜像里的系统用户
999100999常见服务账号
65535165535一段范围的上限

实际完成这段映射设置的,是 newuidmap 和 newgidmap 两个带 setuid 的小工具,它们由 uidmap 软件包提供,这也是后文安装前置依赖里必须有它的原因。

一句话总结原理:Rootless 不是把权限做小了,而是把容器里的 root 翻译成了宿主机上的普通人。

WARNING

用户命名空间本身也曾出过本地提权漏洞,所以 rootless 是「多一层隔离」,不是「绝对安全」。宿主机内核要保持更新,这个前提在任何模式下都成立。

三、Rootless Docker 安装实战#

以下以 Debian / Ubuntu 系、普通用户 anex 为例。整个过程不需要先卸载标准 Docker,但两者不要在同一用户下混用,迁移注意事项见第八节。

3.1 前置依赖#

先装齐依赖,缺一个后面都会卡:

Terminal window
sudo apt update
sudo apt install -y uidmap dbus-user-session slirp4netns fuse-overlayfs

逐个说明它们干什么:

依赖作用
uidmap提供 newuidmap / newgidmap,完成 subuid 映射
dbus-user-session让普通用户能跑 systemd --user 用户级服务
slirp4netns给 rootless 容器提供用户态网络,是默认的网络实现
fuse-overlayfs普通用户无法挂载 overlayfs 时的替代存储驱动,性能略差但兼容性好

确认 subuid 已分配:

Terminal window
grep "^$(whoami):" /etc/subuid /etc/subgid

正常会输出两行各 65536 的范围。如果为空,用下面命令手动补(把 anex 换成你的用户名):

Terminal window
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 anex

再确认内核允许普通用户创建用户命名空间(Debian 默认允许,个别加固过的系统会关):

Terminal window
sysctl kernel.unprivileged_userns_clone
# 期望输出 1;若为 0:
sudo sysctl -w kernel.unprivileged_userns_clone=1

3.2 取得安装脚本#

Rootless 模式的安装脚本随 Docker 官方静态包分发,也在官方仓库里提供。最稳妥的路径是装好官方源之后,用包管理器安装 rootless 扩展包:

Terminal window
sudo apt install -y docker-ce-rootless-extras

装完后,当前用户目录下可直接调用脚本(注意:不要加 sudo,全程用普通用户执行):

Terminal window
dockerd-rootless-setuptool.sh install

脚本会自动做这些事:检查依赖与 subuid、生成 ~/.config/docker/daemon.json、写入用户级 systemd 单元 ~/.config/systemd/user/docker.service、把 DOCKER_HOST 的提示打在输出末尾。

3.3 配置环境变量并启动#

Rootless 的 socket 不在 /var/run/docker.sock,而在用户自己的运行时目录里。把这两行加进 ~/.bashrc(或你用的 Shell 配置文件):

Terminal window
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

重新登录或 source ~/.bashrc 后,用用户级 systemd 启动和管理:

Terminal window
systemctl --user start docker
systemctl --user enable docker
systemctl --user status docker

验证一把:

Terminal window
docker info | grep -i rootless
docker run --rm hello-world
docker run -d --name web -p 8080:80 nginx
curl http://127.0.0.1:8080

docker info 里能看到 rootless 字样、且 hello-world 与 Nginx 都正常,就装好了。日常命令和标准 Docker 完全一致,docker ps、docker compose up -d 该怎么用还怎么用。

3.4 让退出登录后服务不死:enable-linger#

用户级 systemd 有个默认行为:用户最后一个登录会话退出后,它的用户服务会被一并停掉。服务器场景这显然不行,容器必须常驻。解决办法是给该用户开启 linger:

Terminal window
sudo loginctl enable-linger anex
loginctl show-user anex | grep Linger
# 输出 Linger=yes 即生效

开启后,即使 SSH 断开、用户完全退出,systemctl --user 管理的 Docker 和容器也会继续运行,开机后也会自动拉起。这一步漏掉的话,会出现「一退出 SSH 容器全没了」的灵异现象,务必在交付前检查。

四、Rootless Docker 的限制清单#

Rootless 的限制大多来自「你只是个普通用户」这一事实。每条都给解法,能接受再上:

限制原因解法
默认绑不了 1024 以下端口普通用户无权绑定特权端口改用 8080/8443 再由反代转发;或放宽限制:sudo sysctl -w net.ipv4.ip_unprivileged_port_start=0 并写入 /etc/sysctl.conf 持久化
容器里 ping 可能不通ICMP 需要 ping_group_range 许可sudo sysctl -w net.ipv4.ping_group_range="0 2147483647",同样建议持久化
资源限制不完整cgroup v1 下普通用户无权管理 cgroup宿主机切到 cgroup v2(统一层级),docker info 里确认 Cgroup Version: 2 后,--memory、--cpus 等限制才完整可用
存储驱动性能不能挂载内核 overlayfs 时会退到 fuse-overlayfs内核与发行版较新时优先用原生 overlayfs(rootless 已支持);老系统接受 fuse-overlayfs 的性能损耗,或把数据卷放在快盘上
网络性能略低默认走 slirp4netns 用户态转发对延迟和吞吐敏感的服务先实测;新版本可关注 rootlesskit 的加速选项,达不到要求就用标准模式
部分需要特权的操作不可用如 --privileged、--network host 语义受限这类需求本质与 rootless 冲突,单独评估,一般结论是这类服务不适合放 rootless
AppArmor 等主机级配置普通用户改不了宿主机安全模块需要改宿主机配置的部分仍要管理员配合一次
WARNING

把 net.ipv4.ip_unprivileged_port_start 改成 0 意味着这台机器上任何普通用户都能绑 80/443 等低端口,这是拿一点宿主机安全性换便利。多用户共用的机器上请慎用,单人服务器相对可控。

总体判断:纯 Web 服务、API、个人项目这些主流场景,rootless 的限制基本都能绕过去;重度依赖底层网络和高性能存储的场景,要先小规模实测再决定。

五、Podman:干脆不要守护进程#

如果说 Rootless Docker 是「守护进程还在,只是换成普通用户跑」,那 Podman 的思路更彻底:根本不要守护进程。

5.1 fork/exec 模型#

Podman 由红帽主导开发。它的命令直接以当前用户身份 fork/exec 出容器进程,没有一个常驻的、权限很高的中间人:

  • 你是谁,容器就以谁的权限跑,天然默认 rootless
  • podman 进程执行完就退出,不留常驻进程,攻击面随之消失
  • 也提供了一个可选的 podman.socket 兼容接口给工具调用,但那是按需的,不是必需品

这带来的直接好处是和 systemd 的集成非常自然:容器就是普通进程,systemd 可以直接管它的生命周期、日志和重启,不需要再套一层守护进程去对接。

5.2 Pod 概念与 Kubernetes 的渊源#

Podman 有一个 Docker 没有的一等概念:Pod。一个 Pod 里可以放多个容器,它们共享同一个网络命名空间,也就是共享 IP 和端口空间、可以互相用 localhost 访问。

这个概念正是从 Kubernetes 借来的——Kubernetes 里最小的调度单位就是 Pod。于是 Podman 多了一条独特的工作流:本地用 Podman 把一组容器跑顺之后,可以用 podman generate kube 把它导出成 Kubernetes 的 YAML,再拿去集群里部署;反过来也能 podman play kube 直接按 YAML 起容器。对有朝一日要碰 Kubernetes 的人,这条桥很顺手。

5.3 与 systemd 集成:从 generate systemd 到 Quadlet#

老办法是生成 systemd 单元文件:

Terminal window
podman generate systemd --name web --files --new

它会吐出一个 .service 文件,放进用户或系统的 systemd 目录后,就能用 systemctl 管理容器启停。需要注意:generate systemd 在新版本 Podman 里已经标记为不推荐继续使用,官方主推的是 Quadlet。

Quadlet 的写法是声明式的:你写一个 web.container 文件,描述镜像、端口、卷,长这样:

[Container]
Image=nginx:latest
PublishPort=8080:80
[Service]
Restart=always
[Install]
WantedBy=default.target

把它放到 ~/.config/containers/systemd/,执行 systemctl --user daemon-reload 后,systemd 会自动生成对应的服务,之后 systemctl --user start web 即可。配置即代码、开机自启、日志进 journal,一套全齐,这是目前 Podman 官方推荐的常驻服务方式。

六、Podman 上手与命令对照#

Podman 在命令行层面高度兼容 Docker,迁移成本主要在习惯,不在语法:

操作DockerPodman备注
拉镜像docker pull nginxpodman pull nginx完全一致
起容器docker run -d -p 8080:80 nginxpodman run -d -p 8080:80 nginx参数基本通用
看容器docker pspodman ps一致
看镜像docker imagespodman images一致
看日志docker logs webpodman logs web一致
进容器docker exec -it web shpodman exec -it web sh一致
构建镜像docker build -t app .podman build -t app .底层是 Buildah,Dockerfile 通用
编排docker compose up -dpodman compose up -d见下文说明
系统信息docker infopodman info输出字段略有差异

正因为兼容到这个程度,很多人直接在 Shell 配置里写一行别名先用起来:

Terminal window
alias docker=podman

绝大多数日常命令和脚本不用改就能跑。差异主要出现在少数高级参数、以及依赖 docker.sock 的第三方工具上——那些工具需要改走 podman.socket,兼容性要逐个验证。

Podman Compose 的现状要说清:早期是独立的 Python 项目 podman-compose,功能追赶中;现在的 Podman 自带 podman compose 子命令,它会调用系统里可用的 Compose 提供者(可以是 Docker Compose,也可以是 podman-compose)。常规的 services、volumes、networks、ports、environment 这些主流字段兼容度已经不错,但涉及 deploy、复杂网络和某些扩展字段时仍可能有出入。从 Docker 迁过去的 Compose 文件,先在测试环境完整跑一遍再上生产,这是稳妥做法。

补一个 rootful 的说明:Podman 也能用 sudo podman 以 root 方式运行,但它的设计初衷和最大优势都在 rootless。用 Podman 还全程加 sudo,等于把它的核心卖点丢了一半。

七、三方选型对比#

维度标准 DockerRootless DockerPodman
守护进程有,以 root 常驻有,以普通用户运行无守护进程(socket 可选)
逃逸后果直接宿主机 root降为普通用户降为普通用户(默认 rootless)
命令兼容本体与标准 Docker 完全一致高度兼容,少数高级参数有差异
Compose 生态最成熟与标准 Docker 完全一致可用,复杂文件需验证
第三方工具生态最丰富,面板/脚本默认支持多数支持,需改 socket 路径依赖 docker.sock 的工具要适配
systemd 集成靠 Docker 自己的重启策略用户级 systemd 加 linger最原生,Quadlet 声明式管理
限制无特殊限制低端口、cgroup、网络有若干限制个别内核特性与网络场景需验证
运维成本最低,大家都熟中等,要懂用户命名空间中等,红帽系资料最全

明确建议,分场景给结论:

  • 个人单机服务器、就自己用:默认继续用标准 Docker 就好,生态省心;但务必把安全基本功做足——不乱挂 docker.sock、不随便 --privileged、镜像来源可控。这些在 /posts/docker-security-resources/ 里都写过。
  • 要跑第三方服务、多人共用、或安全要求明确:优先 Rootless Docker。命令和 Compose 零学习成本,风险面实打实变小,限制也基本有解。
  • 红帽系发行版(RHEL / Rocky / AlmaLinux)、重度 systemd 用户、或有 Kubernetes 规划:Podman 是更顺的选择,发行版原生支持最好,Pod 到 K8s 的路径也最短。
  • 依赖大量 Docker 生态工具(某些面板、监控、自动化脚本写死了 docker.sock):先别硬迁,逐个验证兼容性再决定,生产环境不要在没验证的情况下直接替换容器运行时。

八、迁移注意事项#

从标准 Docker 切到 rootless 或 Podman,有三个坑提前知道:

  1. **镜像和卷不互通。**三套环境各有自己的存储目录:标准 Docker 在 /var/lib/docker,Rootless Docker 在 ~/.local/share/docker,Podman 在 ~/.local/share/containers。切过去之后 docker images 看到的是空的,需要重新 pull,或用 docker save / docker load 把镜像包搬过去;数据卷里的数据要单独导出导入,别指望原地继承。

  2. **Compose 文件要回归测试。**Rootless Docker 用的是同一套 Compose,基本无感;Podman 侧如第六节所说,主流字段没问题,边角字段(扩展网络、特殊设备映射、健康检查细节)逐项验。

  3. **别在同一台生产机上长期混装两套。**调试期并存没问题,但长期混装会带来端口占用分不清、镜像版本对不上、排障时不知道命令打到哪套环境的问题。选定一套,把另一套的容器迁完就清理干净。特别是 socket 路径:标准模式是 /var/run/docker.sock,rootless 是 /run/user/<uid>/docker.sock,DOCKER_HOST 指错地方时会出现「命令能跑但容器列表是空的」这种误导性现象,先查这个变量。

迁移节奏建议:先在一台测试机或一个不重要的服务上试点,跑满一两周,把端口、备份、自动更新、监控这些周边都接回去,确认无坑后再迁主力服务。

九、排障小节#

9.1 Rootless Docker 装完起不来,先查 XDG_RUNTIME_DIR#

典型报错是启动时提示找不到运行时目录或 socket 路径异常。多见于没通过正常登录会话、直接用 su 切用户或在某些 cron / 脚本环境里执行。

排查顺序:

Terminal window
echo $XDG_RUNTIME_DIR
# 应输出 /run/user/1000 这类路径,为空就是问题所在
ls -ld /run/user/$(id -u)
systemctl --user status docker
journalctl --user -u docker -n 50

XDG_RUNTIME_DIR 为空时,先退出去、用正常方式重新 SSH 登录该用户再试;仍不行,检查 pam_systemd 是否正常工作、以及该用户的 linger 是否已开。

9.2 端口绑不上:permission denied 或对外不通#

分两种情况:

Terminal window
# 情况一:绑 80/443 直接报权限错误
sysctl net.ipv4.ip_unprivileged_port_start
# 输出 1024 说明特权端口下限没放宽,按第四节处理,或改走高位端口加反代
# 情况二:容器起来了但外部访问不通
docker port web
curl -v http://127.0.0.1:8080
ss -tlnp | grep 8080

本机 curl 不通先看容器本身和端口映射;本机通、外网不通,再查宿主机防火墙与云厂商安全组,这与 rootless 无关,别在容器里绕圈子。

9.3 systemd 用户服务一退出登录就停#

症状非常典型:人一断开 SSH,容器全停;重新登录后服务显示 inactive。这就是 linger 没开:

Terminal window
loginctl show-user $(whoami) | grep Linger
# Linger=no 就是它
sudo loginctl enable-linger $(whoami)
systemctl --user enable docker
systemctl --user start docker

顺手确认 docker.service 是用户级单元而不是系统级:文件应在 ~/.config/systemd/user/docker.service,命令也始终是 systemctl --user,漏了 --user 会打到系统级 systemd 上,报找不到单元。

9.4 Podman 侧两个常见小问题#

拉镜像报 short-name 未解析,是因为 Podman 默认要求完整镜像名。把 nginx 写全为 docker.io/library/nginx 即可,或在 /etc/containers/registries.conf 里配置 unqualified-search-registries。

普通用户跑容器时存储报错、提示 overlay 挂载失败,一般是内核或发行版不支持 rootless overlay,切到 fuse-overlayfs 即可,在 ~/.config/containers/storage.conf 里把 driver 改为 overlay 并确保已装 fuse-overlayfs,或直接使用 vfs 先跑通(性能最差,只作临时验证)。

小结#

这一篇把「不用 root 跑容器」的两条路线讲完了:

  • 为什么:标准 Docker 的守护进程与容器 root 权限过大,逃逸即 root、docker.sock 即 root,是最值得先堵的口子
  • Rootless Docker:靠用户命名空间把容器 root 映射成宿主机普通用户,命令与 Compose 零变化,代价是低端口、ping、cgroup v2 等几条可解的限制;装完别忘了 loginctl enable-linger
  • Podman:更进一步取消守护进程,默认 rootless,与 systemd 和 Kubernetes 的衔接最顺,命令高度兼容 Docker,但依赖 docker.sock 的工具要逐个验证
  • 选型:个人单机重生态就标准 Docker 加安全基本功,安全优先选 Rootless Docker,红帽系与 K8s 路线选 Podman

无论选哪条,都记住一句话:rootless 是纵深防御的一层,不能替代镜像来源审查、最小权限和及时更新。把这一篇和安全篇配合起来看,你的容器安全体系才算完整。

Rootless Docker 与 Podman:不用 root 的容器路线
https://blog.anexns.com/posts/docker-rootless-podman/
作者
Anexns
发布于
2026-11-15
许可协议
CC BY-NC-SA 4.0