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 去执行的。这带来三个现实问题:
| 风险点 | 具体含义 |
|---|---|
| 守护进程是 root | dockerd 本身或它拉起的组件一旦出漏洞,攻击者直接拿到宿主机 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 的用户本人 |
| 1 | 100001 | 从 subuid 段顺延 |
| 33 | 100033 | 镜像里的系统用户 |
| 999 | 100999 | 常见服务账号 |
| 65535 | 165535 | 一段范围的上限 |
实际完成这段映射设置的,是 newuidmap 和 newgidmap 两个带 setuid 的小工具,它们由 uidmap 软件包提供,这也是后文安装前置依赖里必须有它的原因。
一句话总结原理:Rootless 不是把权限做小了,而是把容器里的 root 翻译成了宿主机上的普通人。
WARNING用户命名空间本身也曾出过本地提权漏洞,所以 rootless 是「多一层隔离」,不是「绝对安全」。宿主机内核要保持更新,这个前提在任何模式下都成立。
三、Rootless Docker 安装实战
以下以 Debian / Ubuntu 系、普通用户 anex 为例。整个过程不需要先卸载标准 Docker,但两者不要在同一用户下混用,迁移注意事项见第八节。
3.1 前置依赖
先装齐依赖,缺一个后面都会卡:
sudo apt updatesudo 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 已分配:
grep "^$(whoami):" /etc/subuid /etc/subgid正常会输出两行各 65536 的范围。如果为空,用下面命令手动补(把 anex 换成你的用户名):
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 anex再确认内核允许普通用户创建用户命名空间(Debian 默认允许,个别加固过的系统会关):
sysctl kernel.unprivileged_userns_clone# 期望输出 1;若为 0:sudo sysctl -w kernel.unprivileged_userns_clone=13.2 取得安装脚本
Rootless 模式的安装脚本随 Docker 官方静态包分发,也在官方仓库里提供。最稳妥的路径是装好官方源之后,用包管理器安装 rootless 扩展包:
sudo apt install -y docker-ce-rootless-extras装完后,当前用户目录下可直接调用脚本(注意:不要加 sudo,全程用普通用户执行):
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 配置文件):
export PATH=/usr/bin:$PATHexport DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock重新登录或 source ~/.bashrc 后,用用户级 systemd 启动和管理:
systemctl --user start dockersystemctl --user enable dockersystemctl --user status docker验证一把:
docker info | grep -i rootlessdocker run --rm hello-worlddocker run -d --name web -p 8080:80 nginxcurl http://127.0.0.1:8080docker info 里能看到 rootless 字样、且 hello-world 与 Nginx 都正常,就装好了。日常命令和标准 Docker 完全一致,docker ps、docker compose up -d 该怎么用还怎么用。
3.4 让退出登录后服务不死:enable-linger
用户级 systemd 有个默认行为:用户最后一个登录会话退出后,它的用户服务会被一并停掉。服务器场景这显然不行,容器必须常驻。解决办法是给该用户开启 linger:
sudo loginctl enable-linger anexloginctl 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 单元文件:
podman generate systemd --name web --files --new它会吐出一个 .service 文件,放进用户或系统的 systemd 目录后,就能用 systemctl 管理容器启停。需要注意:generate systemd 在新版本 Podman 里已经标记为不推荐继续使用,官方主推的是 Quadlet。
Quadlet 的写法是声明式的:你写一个 web.container 文件,描述镜像、端口、卷,长这样:
[Container]Image=nginx:latestPublishPort=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,迁移成本主要在习惯,不在语法:
| 操作 | Docker | Podman | 备注 |
|---|---|---|---|
| 拉镜像 | docker pull nginx | podman pull nginx | 完全一致 |
| 起容器 | docker run -d -p 8080:80 nginx | podman run -d -p 8080:80 nginx | 参数基本通用 |
| 看容器 | docker ps | podman ps | 一致 |
| 看镜像 | docker images | podman images | 一致 |
| 看日志 | docker logs web | podman logs web | 一致 |
| 进容器 | docker exec -it web sh | podman exec -it web sh | 一致 |
| 构建镜像 | docker build -t app . | podman build -t app . | 底层是 Buildah,Dockerfile 通用 |
| 编排 | docker compose up -d | podman compose up -d | 见下文说明 |
| 系统信息 | docker info | podman info | 输出字段略有差异 |
正因为兼容到这个程度,很多人直接在 Shell 配置里写一行别名先用起来:
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,等于把它的核心卖点丢了一半。
七、三方选型对比
| 维度 | 标准 Docker | Rootless Docker | Podman |
|---|---|---|---|
| 守护进程 | 有,以 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,有三个坑提前知道:
-
**镜像和卷不互通。**三套环境各有自己的存储目录:标准 Docker 在
/var/lib/docker,Rootless Docker 在~/.local/share/docker,Podman 在~/.local/share/containers。切过去之后docker images看到的是空的,需要重新pull,或用docker save/docker load把镜像包搬过去;数据卷里的数据要单独导出导入,别指望原地继承。 -
**Compose 文件要回归测试。**Rootless Docker 用的是同一套 Compose,基本无感;Podman 侧如第六节所说,主流字段没问题,边角字段(扩展网络、特殊设备映射、健康检查细节)逐项验。
-
**别在同一台生产机上长期混装两套。**调试期并存没问题,但长期混装会带来端口占用分不清、镜像版本对不上、排障时不知道命令打到哪套环境的问题。选定一套,把另一套的容器迁完就清理干净。特别是 socket 路径:标准模式是
/var/run/docker.sock,rootless 是/run/user/<uid>/docker.sock,DOCKER_HOST指错地方时会出现「命令能跑但容器列表是空的」这种误导性现象,先查这个变量。
迁移节奏建议:先在一台测试机或一个不重要的服务上试点,跑满一两周,把端口、备份、自动更新、监控这些周边都接回去,确认无坑后再迁主力服务。
九、排障小节
9.1 Rootless Docker 装完起不来,先查 XDG_RUNTIME_DIR
典型报错是启动时提示找不到运行时目录或 socket 路径异常。多见于没通过正常登录会话、直接用 su 切用户或在某些 cron / 脚本环境里执行。
排查顺序:
echo $XDG_RUNTIME_DIR# 应输出 /run/user/1000 这类路径,为空就是问题所在ls -ld /run/user/$(id -u)systemctl --user status dockerjournalctl --user -u docker -n 50XDG_RUNTIME_DIR 为空时,先退出去、用正常方式重新 SSH 登录该用户再试;仍不行,检查 pam_systemd 是否正常工作、以及该用户的 linger 是否已开。
9.2 端口绑不上:permission denied 或对外不通
分两种情况:
# 情况一:绑 80/443 直接报权限错误sysctl net.ipv4.ip_unprivileged_port_start# 输出 1024 说明特权端口下限没放宽,按第四节处理,或改走高位端口加反代
# 情况二:容器起来了但外部访问不通docker port webcurl -v http://127.0.0.1:8080ss -tlnp | grep 8080本机 curl 不通先看容器本身和端口映射;本机通、外网不通,再查宿主机防火墙与云厂商安全组,这与 rootless 无关,别在容器里绕圈子。
9.3 systemd 用户服务一退出登录就停
症状非常典型:人一断开 SSH,容器全停;重新登录后服务显示 inactive。这就是 linger 没开:
loginctl show-user $(whoami) | grep Linger# Linger=no 就是它sudo loginctl enable-linger $(whoami)systemctl --user enable dockersystemctl --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 是纵深防御的一层,不能替代镜像来源审查、最小权限和及时更新。把这一篇和安全篇配合起来看,你的容器安全体系才算完整。