首页 / 资讯中心 / 文章详情

从 Docker 到 Podman:Rootless 架构迁移避坑实录

从 Docker 到 Podman:Rootless 架构迁移避坑实录 ★ FEATURED ARTICLE
最近在处理一套旧服务器的容器环境时开发那边提了个需求不想把每个同事都加进docker组希望普通用户也能自己跑容器、自己清理镜像尽量别碰sudo。这个需求听着不复杂但在 Docker 上落地就很别扭——给普通用户直接操作容器几乎等同于把root权限交出去。于是我把目光转向了 Podman。差不多用了一个月时间我把原先那套用docker compose编排的项目整体迁到了 Podman 上期间踩了 rootless 挂载、端口映射、日志驱动几个不小的坑也借此把 Podman 和 Docker 在架构层面的差异彻底摸了一遍。这篇文章不绕弯子先讲清 Podman 是什么再独家拆 Docker 的守护进程模型和 Podman 的无守护进程模型到底差在哪然后把我实测迁移过程中的坑逐个列出来最后给出选型建议。无论你只是听说过 Podman 想了解对比还是已经在考虑迁移都能从中得到有用的参考。1. 从一次权限需求开始为什么有人想换掉 Docker1.1 一个看似简单却棘手的运维需求先说回开头那个需求。团队里有一批开发人员日常需要起容器做本地联调但服务器不允许每个人都拿到sudo。当时有两个方向给每个人单独开通账号再把所有人加进docker组用专门的 CI/CD 机器统一跑容器开发人员只提交代码不碰服务器。第一个方向最省事但问题在于 Docker 的权限模型很粗放docker组成员等价于拥有宿主机root权限只是换了一种形式入口。当时安全同事原话我记不太清了大意是加一个成员进 docker 组基本等于把整台服务器的钥匙复制了一份给他。这不只是理念问题实际发生过一次事故某位开发在容器里挂载了宿主机根目录然后顺手rm -rf虽然目标路径写错了没造成严重后果但整个团队都被吓出一身冷汗。第二个方向又太重周期长不适合当时只有几台机器的小集群。后来我了解到 Podman 的 rootless 模式普通用户不需要任何特权也能跑容器、映射端口、管理镜像。这正好切中了需求点。于是我开始认真研究 Podman 到底是什么以及它能否做到对现有工作流的无感替换。1.2 Podman 不是 Docker 的改版而是另一条路线上的产物很多人第一次听到 Podman会下意识以为它是 Docker 的一个分支或者平替。其实从设计源头开始两者就走的是完全不同的路线。Docker 是一个完整的容器平台核心组件包括 CLI 客户端、常驻守护进程、镜像构建、容器编排等。而 Podman 从一开始就定位为无守护进程的容器引擎它产自 Linux 容器生态中的另一支脉络与 Buildah镜像构建、Skopeo镜像管理等工具协同工作。容器运行时底层依赖的是 OCI 标准和runc这一点两者是共通的但在上层架构上分道扬镳。打个比方Docker 像一个管家模式所有容器都由一个常驻后台程序统一管理你只需要给管家发指令Podman 则是直接干活模式没有管家你一条命令下去Podman 进程自己把容器拉起来然后把容器的后续托管交给一个轻量监控进程。这个差异看起来简单实际影响非常深远——权限模型、进程模型、系统集成方式全都被它牵动。2. 架构差异是根源守护进程、fork-exec 与 Rootless2.1 Docker 的客户端-守护进程模型是怎么运作的要理解 Docker先理解守护进程这四个字的分量。Docker 安装完成后系统里会常驻一个dockerd进程。你执行的docker ps、docker run这些命令本质上都是 CLI 客户端通过 socket 或 HTTP 接口发送一串 JSON 指令给dockerd由这个守护进程统一去操作镜像、创建容器、管理网络和存储卷。这种架构优点明显所有容器状态集中管理API 清晰团队协作时也方便通过远程 API 访问。但它有几个隐藏问题第一dockerd是常驻进程且拥有管理容器的全部权限。只要拿到了dockerd的访问权比如加入了docker组就基本拿到了宿主机控制权。这个问题在单机多用户场景下非常棘手。第二守护进程成为单点。一旦dockerd崩溃或进入异常状态宿主机上所有容器都会受到影响。虽然现代 Docker 版本做了不少稳定性改进但我确实遇到过dockerd假死、容器明明还在跑但docker ps无响应的情况处理方式只能是重启守护进程连带幸存容器一起被波及。第三守护进程的运行需要root权限。这意味着正常情况下普通用户无法直接与 Docker 交互只能通过用户组或者特殊配置绕行而绕行的代价就是权限放大。2.2 Podman 的无守护进程架构fork-exec 和 conmonPodman 的设计思路是一个容器一个进程树。当你执行podman run时Podman 进程会直接以fork方式创建子进程再通过exec将容器运行时如runc加载进来最终启动容器内部进程。真正耐人寻味的是容器启动之后。Docker 会用守护进程持续跟踪每个容器的状态而 Podman 则引入了conmon这个轻量监控进程。conmon是每个容器独立配套的观察员只负责几个核心任务转发容器日志、维护容器主进程的退出状态、处理信号转发。它不干预容器运行也没有管理其他容器的能力。这意味着 Podman 启动的每一个容器都和当前用户的进程树直接关联。你登录服务器以某个用户身份执行podman run容器进程就是你当前 shell 的子进程。注销登录后容器是否继续运行取决于启动方式容器崩溃也不会拖累其他容器因为没有中央管理员。对于系统管理员来说这个模型最大的好处是不再存在一个权限极高又常年驻留的守护进程风险面被拆散了。同时如果你在服务器上通过 SSH 运行 Podman断开连接后可以用systemd --user配置容器服务的托管做到自动拉起和开机启动和系统服务的结合会比守护进程模型更自然。2.3 Rootless 模式下容器到底怎么跑起来的Rootless 是 Podman 绕不开的亮点但它的实现原理比听起来复杂也是实际使用中产生为什么我跑不了 xx 功能的根源之一。Rootless 运行的本质是使用当前用户的用户命名空间user namespace在命名空间内部当前普通用户被映射为容器内的root用户。容器里看到的是 UID 0宿主机上对应的却是你自己的普通 UID比如 1000完全没有获得宿主机的特权。为了实现这一点Linux 内核给每个普通用户预留了子 UID 和子 GID 范围配置在/etc/subuid和/etc/subgid文件中。比如demo:100000:65536这行记录的意思是用户demo可以使用的子 UID 从 100000 开始一共 65536 个。容器内的 UID 0 会映射到宿主机的 100000容器内的 UID 1 映射到 100001以此类推。与之配套的还有存储驱动。普通用户对宿主机文件系统的权限有限rootless 模式下容器镜像的存储通常会采用fuse-overlayfs或者vfs驱动。前者性能接近原生 overlayfs但依赖 FUSE 用户态文件系统后者实现简单但性能较差、占空间高。网络层面同样有变化。rootless 容器没有直接的网络设备创建权限无法像 rootful 模式那样直接创建 bridge 网络通常通过slirp4netns或pasta这类用户态网络工具实现网络隔离和端口转发。这也解释了为什么 rootless 容器访问外网的速度有时不及 rootful 模式——数据包多经过了一层用户态转发。这一整套映射机制复杂但它换来的价值是普通用户可以完整使用容器功能同时不会因为操作容器而获得宿主机任意文件读写能力。安全性从架构层面得到了提升而不是依赖自觉和权限审批。2.4 关键维度对比表对比维度DockerPodman架构模型客户端-守护进程CLI dockerd无守护进程fork-exec conmon默认权限要求需要 root 或 docker 组权限兼容 rootful也支持普通用户 rootless容器生命周期由 dockerd 统一维护由容器自身的 conmon 监控进程维护单点故障风险dockerd 异常影响所有容器无中央守护进程风险隔离与 systemd 集成需要额外工具或手动配置原生友好可以用 systemd 用户服务托管CLI 兼容性标准 docker 命令兼容 docker 命令大部分可 alias 替换容器编排Compose、Swarm 等Pod、podman-compose、兼容 Docker API底层运行时基于 containerd / runc基于 conmon / runc遵循 OCI表格只列了核心维度。很多细分的差异会在真实操作中进一步暴露下面几节会展开讲。3. 命令兼容不等于零成本迁移Socket、Pod 和 systemd3.1 alias dockerpodman 背后的 CLI 对齐网上流传最广的一句话是装完 Podman 后直接alias dockerpodman就行。这句话大方向没错但真做起来需要知道它为什么能成立、边界在哪。Podman 的 CLI 设计刻意向 Docker 靠齐。docker run、docker ps、docker images、docker exec、docker stop这些高频命令换成podman前缀后参数和用法几乎一致。社区做了大量细节对齐工作比如--rm、-v、-p、--env-file的解析行为都和 Docker 尽量保持一致。但这不表示所有命令都完全等价。有两个典型差异容易踩中docker compose是 Docker 官方内置的子命令Podman 默认没有。你需要额外安装podman-compose或者通过兼容 Socket 让docker compose直接访问 Podman。某些 Docker 私有扩展参数在 Podman 里不被支持或者实现方式不同比如--gpus、--cpuset-mems这类高级调度参数需要根据版本逐一验证。我的建议是建立迁移测试清单把项目在 CI 里使用的所有 docker 命令都列出来逐一在 Podman 上跑一遍而不是上来就 alias。至少在我测试过的项目中90% 的常用命令可以直接对齐剩下的 10% 才是需要投入精力的地方。3.2 利用 podman.sock 兼容 Docker API让现有工具继续工作项目迁移过程中最担心的是CI 脚本是别人写的里面调用了 Docker API或者你准备继续使用 Docker Compose 管理容器不想改写全部脚本。Podman 提供了一个很实用的能力——兼容 Docker API 的 Socket。启动方式略有多种常见的一种是systemctl --user enable --now podman.socket启用后默认会在用户目录下生成一个podman.sock文件。然后在环境变量或 Docker CLI 配置中指定使用这个 Socketexport DOCKER_HOSTunix://$XDG_RUNTIME_DIR/podman/podman.sock docker ps此时你看到的docker ps结果实际来自 Podman 的容器列表。很多依赖 Docker API 的工具包括docker compose的某些版本都能通过这个方式继续工作不用改代码。但要注意这是兼容层不是完全等价层。个别 API 端点返回的数据字段和 Docker 原生略有差异依赖这些字段做自动化脚本的话最好小范围先测一遍。我遇到过一次情况某个自动化部署脚本读取 API 返回的NetworkSettings.IPAddress字段在 Docker 上能拿到容器 IP在 Podman 兼容 API 下却拿到了空值排查了很久才知道需要改用--networkslirp4netns模式下额外读取端口映射数据。这类边缘差异不影响日常操作但自动化程度高的团队要提前摸底。3.3 Pod 概念和 systemd 集成比单容器更进一步Podman 真正的差异化能力不完全在替代 Docker 命令而是引入了 Pod 概念和 systemd 集成。Pod 是从 Kubernetes 借鉴过来的概念一组容器共享同一个网络命名空间、存储卷和端口资源。用 Podman 启动一个 Pod再在这个 Pod 里加入多个容器它们可以直接通过localhost互相访问不需要额外走端口映射。这在某些场景下非常实用。例如开发一个前后端分离的应用你希望 Nginx 容器和 API 服务容器共享网络栈用 Docker 需要创建自定义 bridge 网络再手动指定别名用 Podman 只需先建一个 Pod两个容器先后加入即可彼此访问就是localhost:8080这种形式。systemd 集成则是运维侧的亮点。普通用户跑 rootless 容器时可以用systemd --user服务来管理容器的自动启动和崩溃重启。生成一个系统服务单元文件的方法podman generate systemd --name mycontainer --files --new这个命令会根据现有容器生成对应的 service 文件放在用户级 systemd 目录下然后执行systemctl --user daemon-reload systemctl --user enable --now container-mycontainer.service容器就像 systemd 服务一样随用户登录自动拉起。这种管理方式面向的其实是长期运行的服务器服务比单纯docker restart策略更贴近系统原生的服务治理体系。3.4 迁移时最容易忽略的差异点这里列出几个我实测迁移时容易忽略、但影响面不小的差异点镜像存储位置不同Docker 默认在/var/lib/dockerPodman rootless 模式下在用户目录下比如~/.local/share/containers。如果服务器磁盘分区方案不同可能导致空间不足。日志驱动不同Docker 默认json-filePodman rootless 默认使用journald或k8s-file具体取决于发行版配置。依赖容器日志文件位置的工具会受影响。重启策略语法差异docker run --restartunless-stopped在 Podman 中同样支持但响应信号的方式略有差异需要验证容器退出码和崩溃恢复行为。网络模式命名差异--networkhost、--networkbridge这些通用模式两边一致但--networkcontainer:xxx在 Podman 中会要求目标容器属于同一个用户或同一个 Pod限制比 Docker 更严格。挂载选项差异尤其是 SELinux 环境下Podman 默认可能要求显式添加:Z或:z标记来调整挂载目录的安全上下文否则容器内没有权限读取宿主机文件。这些差异单独看都不大但叠加起来就会转化成迁完才发现有问题的加班时间。我把这个清单当作迁移前的检查项提前和团队过一遍后面顺了很多。4. 一套 Compose 项目迁移 Podman 的实测记录4.1 当时的环境和准备我迁移的项目是一个典型的三容器应用前端静态页面的 Nginx、后端 API 服务、一个 PostgreSQL 数据库。原先部署在 Docker Compose 上文件里有端口映射、环境变量、卷挂载、健康检查还用了depends_on控制启动顺序。迁移前我做了几件事在测试机上安装最新版 Podman 和podman-compose用podman info确认 rootless 模式和存储驱动已经正确初始化把原 Composer 文件里的 image 拉下来逐个用podman run做冒烟测试用podman generate systemd提前生成服务文件为最终部署做准备。安装过程本身并不复杂在主流 Linux 发行版上包管理器直接安装即可。但有一个细节值得提rootless 模式下需要确保slirp4netns或者pasta已安装否则端口映射会失败。我当时就是因为测试机缺这个组件一度以为网络配置写错了。4.2 踩坑一rootless 模式的端口映射与预期不符第一个明显的坑出在端口映射上。在 Docker 中映射-p 8080:80后宿主机上的netstat能看到监听端口属于dockerd进程访问正常。到了 Podman rootless 模式下同样的命令执行后监听端口的进程变成了slirp4netns或者pasta的用户态进程而且端口默认只会绑定在127.0.0.1上。当时我发现从外部访问服务失败但在服务器本机curl localhost:8080却正常。查下来才知道rootless 模式下普通用户没有绑定低端口的能力也默认不会将端口暴露到所有网络接口需要显式加参数podman run -p 0.0.0.0:8080:80 ...或者在容器配置里指定--networkslirp4netns的端口绑定行为。这个差异对本地开发影响不大但对生产服务器上对外提供服务的场景非常关键。如果忘了加0.0.0.0前缀防火墙开了端口也白搭。另外一个相关问题是端口占用冲突。Docker 会统一管理所有容器的端口分配Podman rootless 模式下每个用户都有自己独立的网络命名空间可能出现两个用户用同一个宿主机端口而不自知导致流量被转给错误的容器进程。多用户共享服务器的场景下端口规划要有统一记录不能指望工具帮你兜底。4.3 踩坑二挂载目录的 UID 映射问题第二个坑更隐蔽也更具代表性。项目里 PostgreSQL 容器需要持久化数据我把宿主机目录挂载进容器volumes: - ./data:/var/lib/postgresql/data在 Docker 中容器进程默认以 root 身份运行挂载目录的权限通常没问题即便有问题dockerd也会用较高权限自动处理。但到了 Podman rootless 模式下容器内的 UID 0 对应宿主机的子 UID比如 100000而挂载目录的实际所有者是你当前用户UID 1000。容器内 PostgreSQL 进程尝试写入这个目录时会被宿主机权限系统拒绝。第一次遇到时报错信息并不直接而是在 PostgreSQL 初始化时出现Permission denied排查了好一会儿才把问题定位到 UID 映射上。解决办法有几个手动把挂载目录的所有者改成对应的子 UID但这样宿主机上你这个用户反而失去了访问权限在容器配置中指定使用--usernskeep-id让容器内的用户保持与宿主机当前用户相同的 UID从而直接读写宿主机目录调整挂载目录的属组和权限将子 UID 范围共享给当前用户组。不同方法适用场景不同。对于用来存数据库数据的目录我更推荐--usernskeep-id方式让容器进程以当前用户的身份运行避免后续备份、归档时还要切换用户身份。对于共享代码目录这类只读场景则可以直接只读挂载省去权限问题。4.4 踩坑三日志驱动和重启策略的差异迁移过程中日志问题一开始没被重视直到做集中日志采集时才发现两边行为不一样。原先 Docker Compose 文件里设置了logging: driver: json-file options: max-size: 10m max-file: 3在 Podman 上json-file日志驱动并不是默认安装的。某些发行版默认只有journald和k8s-file。如果直接沿用 Compose 文件中的日志驱动配置Podman 可能直接报错或者静默使用其他驱动。我当时的做法是统一改用journald因为服务器上本来就有 journald 日志采集配套容器日志和系统日志统一管道管理和排查都方便。重启策略方面Docker 的restart: always语义在 Podman 下有些微妙差别。特别是 rootless 模式下用户注销后用户级 systemd 没有默认启动容器。如果希望容器像系统服务一样崩溃后自动重启建议用podman generate systemd生成服务文件再通过systemctl --user启动。这比单纯依赖容器配置里的restart策略更可靠也更符合服务治理的常规操作。4.5 踩坑四镜像仓库配置与构建缓存最后一个坑不算大但很影响日常效率。Podman 默认使用自己的镜像仓库配置文件通常在/etc/containers/registries.conf或用户目录下的同名文件不会自动继承 Docker 的/etc/docker/daemon.json里的registry-mirrors配置。国内环境下很多人给 Docker 配置过镜像加速。迁移到 Podman 后如果没同步这个配置拉取镜像的速度会明显变慢甚至会超时。需要手动把镜像仓库镜像地址加到registries.conf中[[registry]] location docker.io [[registry.mirror]] location mirror.example.com构建缓存也一样。Docker 的 BuildKit 缓存目录和 Podman 的 Buildah 缓存目录完全不同迁移后第一次构建往往没有缓存可用构镜像时间会明显上升。这是预期内的事情不用慌跑几次后缓存自然就建立起来了。不过 Podman 的 Buildah 也有自己的优势支持--layers和更细粒度的缓存控制还能实现无 root 构建镜像这在某些安全敏感的环境里很有吸引力。5. 选型判断哪些场景该继续用 Docker哪些适合切到 Podman5.1 更适合 Podman 的场景经过一个月的迁移使用我总结出几类比较适合“换到 Podman”的场景。第一类是单机多用户环境。如果你有一台服务器需要让多个开发人员各自跑容器做本地联调又不希望给所有人root权限Podman 的 rootless 模式几乎是量身定做。每个人在自己的用户空间里跑容器互不干扰也不会因为某个用户误操作而影响整台机器。第二类是安全敏感的生产环境。曾经发生过容器逃逸或高危漏洞事件的团队对守护进程的权限集中会比较警惕。Podman 的“无守护进程 用户命名空间”模型把攻击面明显缩小。即使某个容器被攻破攻击者也只能拿到一个低权限用户身份而不是整个宿主机的控制权。第三类是深度使用 systemd 的服务器环境。如果你的基础设施本身就偏传统运维习惯用系统服务管理一切Podman 与 systemd 的天然结合会让容器管理方式融入现有体系而不是另起炉灶。容器运行状态、崩溃重启、开机自启这些能力都能用熟悉的工具链完成。第四类是想要更轻量的容器体验。Podman 不需要一个常驻守护进程内存占用和进程数量都比 Docker 少。在资源紧张的小型 VPS 上这个差距可以感知到。如果你只是跑几个固定服务不希望后台挂一大串进程Podman 会更干净。5.2 继续用 Docker 更省心的场景但我不主张“无脑迁移”。有些场景下继续用 Docker 反而少折腾。如果团队已经深度依赖 Docker Compose 和相关生态并且有大量自动化脚本直接调用 Docker API那么迁到 Podman 需要额外处理兼容层问题。虽然有podman.sock这种方案存在但边缘差异仍然可能存在自动化流程越多踩中差异的概率越大。如果你的日常工作环境是 macOS 或 Windows 桌面Docker Desktop 提供了非常成熟的图形化、文件共享和网络代理方案。Podman 在 macOS 和 Windows 上也有解决方案但成熟度和文档完善度暂时还不如 Docker Desktop 那套体系。多数个人开发者做容器开发Docker Desktop 依然是最省心的选择。如果团队在持续维护 Kubernetes 集群并且大量工作负载已经运行在集群环境中那么本地开发工具用 Docker 保持一致性会更自然。Podman 虽然也支持生成 Kubernetes YAML 和本地模拟 Pod但若整个流程已经围绕 Docker 做了多年沉淀临时切换的收益并不高。还有一个实际原因团队的既有知识库、文档、书籍教程大多以 Docker 命令为示例。新人入职后使用 Docker 的培训成本更低。工具选择不只是技术评估也包含团队可维护性的考量。5.3 我的个人建议和一个小技巧以我这一个月的体验来说Podman 在日常使用中的绝对命令差异其实很小。真正让人需要适应的是“容器归属谁”的思维转变Docker 里所有容器都属于守护进程Podman 里每个容器都属于启动它的用户。这个转变在操作上最直接的体现是用户 A 用podman ps只能看到自己的容器看不到用户 B 的。想查看所有用户的容器需要用到podman ps --external或者去系统层面查进程而不是像 Docker 那样一条命令看全局。如果你的团队有多人共用一台服务器记得提前约定好容器名规范不然排查问题时很难对号入座。最后分享一个小技巧。如果你正在犹豫要不要迁移不需要一次性把整个环境切过去。可以先在这一台机器上同时安装 Docker 和 Podman用podman.sock让部分容器先跑在 Podman 上日常命令还是用熟悉的docker前缀跑上一两周观察兼容性和体验。等确认没问题了再逐步停掉 Docker。这种渐进切换比起“选一天切完”的压力小很多留给团队适应的时间也更充裕。工具选定之后真正重要的还是把容器当作系统工程的一部分来对待权限边界、日志管理、服务托管、备份恢复这些环节不会因为换了引擎而自动变好。Podman 给我最直接的帮助是把“普通用户安全使用容器”这件事从不可能变成了可能而让它跑得稳不稳仍然取决于使用它的人是否理解了背后的设计逻辑。
阅读完成 · 觉得有帮助?
咨询建站