ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Podman 无守护进程容器引擎:Docker 迁移与 rootless 实践

Podman 无守护进程容器引擎:Docker 迁移与 rootless 实践 第一次听到 Podman 的时候我下意识地以为这不过是又一个容器客户端。真正动手把它装到 Fedora、又在一台 Ubuntu 服务器上以普通用户跑起来之后我改变了这种想法。Podman 的核心不只是“能拉镜像、能起容器”而是把整个容器生命周期放回到 Linux 本身的进程、用户和系统服务体系里。没有常驻守护进程不需要管理员权限普通用户也能跑起一个隔离环境。这篇文章面向正在用 Docker、但对 daemon 模式有所顾虑的人也面向想了解现代容器引擎原理的初学者。我会从架构设计讲到实际命令再到 rootless、Pod、systemd 集成和常见坑。所有内容都是我实际试过、踩过之后的记录你可以直接照着操作。1. 为什么容器世界还需要一个“新引擎”1.1 Docker 时代被默认接受的复杂设计Docker 的普及很大程度是因为它把容器操作抽象得足够简单。但你如果深入“docker ps 背后是谁在处理请求”这个问题会发现整个链路并不像表面那么轻docker CLI 请求 dockerddockerd 负责镜像管理、网络、卷、容器状态容器真正运行时又交给 containerd再由 containerd 通过 runc 去和内核的 namespaces/cgroups 打交道。这样的分层设计让 Docker 功能强大但也把大量状态集中在一个特权守护进程里。这个设计在单机开发时很顺手可一旦放到生产环境问题就来了。守护进程本身是 root拥有极高权限它要保持运行升级或重启时所有容器都会受影响如果哪个环节被攻击攻击面会扩大到整个宿主机。用久了你会发现Docker 的 daemon 并不是容器技术的必需条件而是早期为了易用性做成的一种产品形态。很多人默认容器必须由一个后台服务统一管理但 Linux 内核提供的隔离能力本来就和用户会话、进程树紧密相关完全可以换一种更轻的接入方式。1.2 Podman 的定位无守护进程的容器环境Podman 由 Red Hat 主导开发是一款完全基于 Linux 标准的容器引擎。它最大的特点是“无守护进程”也就是说容器生命周期是由你运行的 podman 命令直接管理的而不是由后台服务代管。官方定位是“Drop-in replacement for Docker”大部分命令、镜像格式、端口映射参数都兼容你可以把很多脚本里的 docker 直接替换成 podman。不过要提醒的是这种兼容是 CLI 层面的兼容不是内部实现的照搬。Podman 选择了一种更接近 CRI-O 的方式底层调用 conmon 做监控OCI runtime 负责真正启动进程。它不要求你必须用 root 身份操作rootless 模式下普通用户也能管理自己的容器这正好解决了 Docker daemon 权限过大的一个问题。对我来说最直接的感受是在笔记本上跑容器不用再担心一个 sudo 把整个环境搞乱在服务器上不同用户也可以各自管理自己的容器互不干扰。2. 核心架构没有 daemon容器到底跑在哪里2.1 从 daemon 模型到 fork/exec 模型Docker 的容器进程“属于” dockerddockerd 挂掉容器大多也会跟着遭殃。Podman 则相反当你执行 podman run 时命令行进程会解析配置、准备镜像和存储层然后通过 conmon 拉起一个真正运行容器的子进程。命令退出后conmon 继续持有这个进程并将退出状态挂到 systemd 的 user slice 下。整个过程没有常驻服务也就不存在“重启 podman 服务”这种操作。这个模型带来的直接好处是容器和用户会话之间保持了类似普通进程的管理关系。你在终端里 CtrlC 一个前台容器信号能正确传递在一个服务里启动容器systemd 也可以感知它的状态。如果你在管理多台 Linux 主机会发现这种设计更容易理解和排查——本质上就是“容器进程是谁的孩子”这个问题。用 fork/exec 代替 daemon 后每个容器都拥有独立的父进程链崩溃、重启的影响范围被大大缩小。2.2 幕后真正的“跑者”OCI runtime、conmon、cgroups我们常说 Podman 启动容器但真正和内核交互的并不是 podman 本身。它先准备好 rootfs 和配置然后调用 OCI runtime默认可能是 crun也能用 runc来创建容器进程同时启动 conmon 作为监控进程。conmon 的职责是处理容器的标准输入输出、记录日志、转发信号、监控退出状态。crun 用 C 语言实现启动速度比 runc 快一些资源占用也更低所以在 Fedora/RHEL 上你会看到默认是 crun。另一个容易忽视的角色是 cgroups v2。Podman 在 rootless 模式下会尽量用 cgroups v2 对容器做资源限制配合 systemd 的 delegation让普通用户也能设置 CPU、内存上限。执行 podman info 时可以看到 runtime 名称、存储驱动、cgroup 版本这些关键信息。排查问题前我一般先跑一条 podman info确认环境和预期一致这点在后面对照能省很多时间。Podman 还特别强调 OCI 标准镜像格式、运行时规范都按行业标准走。这意味着它不是另一个“私有容器格式”而是整个容器生态里的一个实现。对用户来说最大价值就是不会被某个厂商锁死拉下来的镜像可以给其他 OCI 运行时用Podman 构建出来的镜像也可以推到任意符合规范的仓库。这种标准化的好处平时感觉不到但当你需要迁移到 Kubernetes、或者在不同 CI 系统间切换时会发现非常重要。3. 上手实践安装、基础命令和 Docker 迁移3.1 不同环境的安装方式Podman 的安装比想象中简单。Fedora/RHEL/CentOS 系直接用 dnfsudo dnf install podmanDebian/Ubuntu 上需要根据版本选择Ubuntu 22.04 以后的仓库里有 podman但版本可能偏旧如果要用新特性建议添加官方源或用发行版容器。macOS 上安装 podman 后还需要 podman machine 创建一个 Linux 虚拟机因为 Podman 本身依赖 Linux 内核特性它不像 Docker Desktop 那样自带一个隐藏 VM而是把 VM 管理明确暴露给你。第一次在 Mac 上跑 podman machine init 的时候会下载一个很小的 Linux 镜像然后通过 socket 连接你依然能用 podman ps 等命令只是底层多了一层 VM。Windows 用户我一般推荐用 WSL2 的 Linux 发行版再安装 podman或者用 podman machine 配合 hypervisor。生产服务器则优先选择 RHEL/Fedora 系因为 Podman 和 SELinux、systemd 的集成在这些发行版上最完整。安装完先跑 podman info 确认没有报错然后可以拉一个 nginx 测试podman run -d -p 8080:80 docker.io/library/nginx:latest这一步能验证网络、存储、运行时是否正常。如果用的是 rootless 模式第一次运行时可能会提示需要配置 subuid/subgid下面第 4 节我会专门展开讲。3.2 把 docker 命令平移到 podman绝大多数场景你只需要做一个 aliasalias dockerpodman然后原有 docker run、docker ps、docker exec、docker logs、docker build 都能继续用。这里有个容易踩的差异Podman 默认镜像仓库也兼容 Docker Hub拉取 public 镜像没问题但私有仓库的认证文件路径可能不同需要留意 ~/.config/containers/registries.conf 和 auth.json 的位置。另一个差异是 docker-composePodman 有 podman compose 子命令它本质上是调用本机的 docker-compose 或 podman-compose并不是内置。我用一张表总结日常命令对照适合快速参考操作DockerPodman拉取镜像docker pullpodman pull查看容器列表docker pspodman ps运行容器docker runpodman run查看日志docker logspodman logs停止容器docker stoppodman stop删除容器docker rmpodman rm构建镜像docker buildpodman build查看磁盘占用docker system dfpodman system df操作层面基本是无痛迁移但我建议不要直接在旧生产环境里无脑 alias而是先在测试环境把镜像跑起来重点检查网络模式、卷挂载和 SELinux 相关行为。毕竟 Podman 对安全上下文的要求比 Docker 更严格老镜像一旦遇到权限问题排查方向会完全不同。我自己就把一套 CI 脚本从 docker 换成了 podman除了个别依赖 docker-compose 的 job 做了调整其他基本没动。4. Rootless 与安全普通用户也能跑容器凭什么4.1 用户命名空间容器里的 root不是主机上的 rootPodman 的 rootless 模式是让我最感兴趣的部分。简单说普通用户启动容器时Podman 会创建一个新的用户命名空间在这个命名空间里当前普通用户被映射成容器内的 UID 0root。所以容器进程以为自己以 root 运行但实际上它在宿主机上仍然是一个普通用户。比如主机上的用户 uid 1000在容器内映射为 0容器内其他 UID 再映射到一组从 100000 开始的子 UID 上。这个机制带来的安全收益很明显即使容器被攻破攻击者拿到的是命名空间里的 root对应到宿主机只是低权限用户。要想访问宿主机的文件还要面对权限和 SELinux 的限制。代价是网络和某些操作更慢尤其是 user-mode networking。平时跑测试影响不大但如果做大量端口转发或高性能网络应用需要提前做 benchmark。如果你在 rootless 模式下遇到容器无法创建多半是没配置用户映射范围。检查 /etc/subuid 和 /etc/subgid里面应该有 username:100000:65536 这样的行。如果没有可以手动加上一行或者安装容器引擎时系统自动生成。配置完后Podman 才有足够的子 UID 去映射容器内不同的用户身份。4.2 能力收紧、SELinux 与 no-new-privileges即使是在 root 模式下Podman 也默认删除了很多 Linux capabilities比如 CAP_SYS_ADMIN、CAP_NET_ADMIN 等容器内进程不能随便加载内核模块、改系统时间。加上 SELinux 的话容器内的 root 还被一个 svirt 类型标签限制写宿主机文件时会有明确拦截。如果你遇到“明明目录权限对但容器里写不进去”的问题先想想 SELinux 标签而不是直接 chmod 777。Rootless 模式下还默认开启 no_new_privs防止通过 setuid 程序提升权限。也就是说容器进程无法通过 suid 二进制拿到更多权限。这些机制叠加起来让 Podman 的默认安全边界比传统 Docker daemon 清晰得多。我在做安全加固时通常会再加几个参数podman run --cap-dropALL --cap-addNET_BIND_SERVICE myapp或者用 --security-opt seccompunconfined 仅在需要调试时使用。生产环境尽量保持默认减少不必要的暴露。如果你真的需要一个特权容器Podman 也允许 --privileged但我会非常谨慎因为它会把能力和 SELinux 限制一并放开等于让容器拥有了宿主机级别的影响能力。5. Pod 管理容器组不是 K8s 的专利5.1 原生 Pod 是什么为什么好用Podman 支持“Pod”概念这一点和 Kubernetes 中的 Pod 类似一组容器共享网络命名空间、UTS 命名空间和 IPC 命名空间。比如你要部署一个 Nginx 代理和一个后端应用传统做法是分别启动两个容器再建立网络。Pod 模式可以直接把它们放进同一个 Pod共享 localhost代理可以轻松访问后端的端口。习惯 Docker 的人可能会问我直接用 docker compose 不也能做容器组吗Docker Compose 的网络编排和 Pod 是有本质区别的。Pod 的容器之间天然共享网络栈容器的 localhost 指向同一个空间Compose 服务虽然在同一网络但 localhost 仍然彼此隔离。Podman 把 Pod 作为一等公民意味着本地跑通的一组容器生成 K8s YAML 后可以直接给集群用开发、测试、生产之间的差距更小。实际操作很简单。先创建一个 Podpodman pod create --name mypod -p 8080:80然后往这个 Pod 里加容器podman run --pod mypod -d --name backend backend-app podman run --pod mypod -d --name frontend nginx这时候两个容器共享 localhostnginx 配置里直接写 proxy_pass http://localhost:8080 就能访问 backend 的服务。如果你一开始没有把端口映射加到 Pod 上后面再加会比较麻烦所以建议在创建 Pod 时就把所有需要暴露的端口指定好。5.2 用 generate kube 和 play kube 连接 KubernetesPodman 的 podman generate kube 和 podman play kube 是很有实用价值的两个命令。执行 podman generate kube mypod 会输出一个符合 Kubernetes 语法的 YAML 文件在本地用 podman play kube mypod.yaml 则可以把这份 YAML 直接在 Podman 环境里跑起来。这就实现了“一份 YAMLK8s 和 Podman 通用”的工作流。实际使用中我一般先在 Podman 里调好参数再 generate 出 YAML检查一下资源限制和探针配置最后部署到集群。反过来也可以拿到团队现成的 K8s Deployment先 podman play kube 在本地验证一遍。这个流程对不上能有 K8s 环境的场景非常友好。要注意的是Podman 只覆盖 K8s 的 Pod、Deployment 等常见资源像 Service、Ingress 这类不会完整支持需要自己补。如果你经常在本地调试 K8s 资源还可以把 generate 出来的 YAML 存进版本库当作一种“可执行文档”。环境更新时直接 play kube 就能拉起一套和线上结构一致的容器组。这个工作流比写一堆 docker run 脚本清晰得多也让团队协作时更容易理解每个容器之间的依赖关系。6. Systemd 集成让容器像系统服务一样管理6.1 用 Podman 生成 unit 文件的两种方式Podman 原生支持把容器交给 systemd 管理常见有两条路一是 podman generate systemd --new --name myweb /etc/systemd/system/myweb-container.service生成的 unit 会在系统启动时用新的容器实例取代旧容器二是新版 Podman 更推荐的 Quadlet通过一个 .container 文件直接声明容器。Quadlet 的好处是配置声明式不依赖预先生成的 systemd unit。例如 /etc/containers/systemd/myweb.container[Unit] DescriptionMy web container [Container] Imagedocker.io/library/nginx:latest PublishPort8080:80 Volume./data:/usr/share/nginx/html:Z [Service] Restarton-failure这样 systemd 会自动根据这个文件实例化服务不需要额外写 ExecStart 等低级字段。和 docker run 相比这种写法的意图更明显镜像、端口、卷都是独立字段改起来也安全。尤其是卷挂载加 :Z会自动处理 SELinux 标签避免很多权限问题。6.2 开机自启和 User 服务的关键点如果要开机自启rootful 容器把上面文件放到 /etc/containers/systemd 即可rootless 容器则放到 ~/.config/containers/systemd/并执行systemctl --user daemon-reload systemctl --user start myweb systemctl --user enable myweb这里有一个非常容易漏掉的细节普通用户的 user 服务默认在用户注销后会停止所以需要执行 loginctl enable-linger 用户名让用户的服务可以脱离登录会话继续运行。在运行极多 podman 容器的主机上systemd 集成几乎是从“手动管理容器”变成“声明式基础设施”的分水岭。容器被杀掉systemd 会按 Restart 策略重启启动顺序可以通过 [Unit] 里的 After 和 Requires 描述。相比脚本里 sleep 等待容器的做法这个方案要可靠得多。日志也会进入 journald一条 journalctl -u myweb 就能看到容器和 systemd 两个层面的输出排查问题非常顺畅。7. 常见问题与排坑手记7.1 rootless 端口映射和网络性能问题很多人在第一步就遇到rootless 容器想映射 80 端口结果报 permission denied。原因是小于 1024 的端口属于特权端口普通进程不能直接绑定。解决办法有几个把宿主机端口改到 8080或者用 sysctl 配置 net.ipv4.ip_unprivileged_port_start80但改宿主内核参数有安全影响尽量不用。另一个典型坑是 rootless 容器之间的网络互通。一般建议用 pasta 或 slirp4netns 做用户态网络老版本默认 slirp4netns转发性能不理想新版可以试试 --networkpastaCPU 开销会低一些。如果追求最大性能可以让容器以 rootful 模式跑或者使用 macvlan/host 模式直连宿主机网络但代价是隔离性减弱。需要在性能和隔离之间做个取舍。实测下来开发机用 rootless 没问题但压测环境我会切到 rootful避免网络转发成为瓶颈。7.2 挂载目录后权限错乱/读不到文件Podman rootless 下挂载宿主目录最常遇到 UID 对不上容器内进程是映射后的 root而宿主机目录属于 uid 1000结果容器写文件很别扭。我的做法是给容器传 --usernskeep-id让容器内用户保留你的宿主 UID或者在挂载卷时加 :Z 参数让 SELinux 自动设置标签。注意 :Z 会自动 relabel 目录如果目录被多个容器共享可能要用 :z否则第二次挂载会因标签冲突失败。如果是线上老镜像还容易出现“宿主机上明明能读容器里却 Permission denied”的情况。这时除了看 UID还要确认 SELinux 拦截日志ausearch -m avc -ts recent能看到具体拒绝原因。直接 chmod 777 是治标不治本还可能弄乱系统安全上下文。根因一般就两个要么是 UID 映射不对要么是 SELinux 标签不对。7.3 alias dockerpodman 不是万能的最后一个排坑点关乎拿来主义。虽然 Podman 兼容大部分 Docker CLI但不是所有生态都无缝。docker-compose 需要通过 podman compose 调用外部插件不一定随时可用docker buildx 也没有完全对应复杂的多阶段缓存差异需要单独适配。此外docker 的 swarm、docker network create 的 overlay 驱动在 Podman 中不是默认能力。如果你只是想本地实验把 docker 换成 podman 是没问题的但要跑生产调度还是应该按 Podman 的方式重新设计网络和服务依赖。我个人碰到过因为 docker-compose 里用了 depends_on 但 Podman 启动顺序不一致导致数据库连不上的小问题后来改用 systemd unit 排序解决。这类兼容差异不是 bug而是设计哲学不同接手项目时心里要有数。我在把主力机器的默认容器引擎切换成 Podman 后最大的变化不是省了多少内存而是排查问题的心态变了。后台没有特权 daemon 背锅容器就是普通进程systemd 看得见、journalctl 查得到普通用户的容器又不会动到系统全局实验空间大了很多。如果你也厌倦了 Docker daemon 带来的那种“黑盒感”不妨从今天开始在测试环境把 docker 换成 podman 跑一周。一开始可能有些不顺但适应之后你会觉得容器本来就该这么用。
返回列表