第一次听到 Podman 的时候,我下意识地以为这不过是又一个容器客户端。真正动手把它装到 Fedora、又在一台 Ubuntu 服务器上以普通用户跑起来之后,我改变了这种想法。Podman 的核心不只是“能拉镜像、能起容器”,而是把整个容器生命周期放回到 Linux 本身的进程、用户和系统服务体系里。没有常驻守护进程,不需要管理员权限,普通用户也能跑起一个隔离环境。这篇文章面向正在用 Docker、但对 daemon 模式有所顾虑的人,也面向想了解现代容器引擎原理的初学者。我会从架构设计讲到实际命令,再到 rootless、Pod、systemd 集成和常见坑。所有内容都是我实际试过、踩过之后的记录,你可以直接照着操作。
1. 为什么容器世界还需要一个“新引擎”
1.1 Docker 时代被默认接受的复杂设计
Docker 的普及很大程度是因为它把容器操作抽象得足够简单。但你如果深入“docker ps 背后是谁在处理请求”这个问题,会发现整个链路并不像表面那么轻:docker CLI 请求 dockerd,dockerd 负责镜像管理、网络、卷、容器状态;容器真正运行时又交给 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 的容器进程“属于” dockerd,dockerd 挂掉,容器大多也会跟着遭殃。Podman 则相反:当你执行 podman run 时,命令行进程会解析配置、准备镜像和存储层,然后通过 conmon 拉起一个真正运行容器的子进程。命令退出后,conmon 继续持有这个进程,并将退出状态挂到 systemd 的 user slice 下。整个过程没有常驻服务,也就不存在“重启 podman 服务”这种操作。
这个模型带来的直接好处是,容器和用户会话之间保持了类似普通进程的管理关系。你在终端里 Ctrl+C 一个前台容器,信号能正确传递;在一个服务里启动容器,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 系直接用 dnf:
sudo 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
绝大多数场景,你只需要做一个 alias:alias docker=podman,然后原有 docker run、docker ps、docker exec、docker logs、docker build 都能继续用。这里有个容易踩的差异:Podman 默认镜像仓库也兼容 Docker Hub,拉取 public 镜像没问题,但私有仓库的认证文件路径可能不同,需要留意 ~/.config/containers/registries.conf 和 auth.json 的位置。另一个差异是 docker-compose,Podman 有 podman compose 子命令,它本质上是调用本机的 docker-compose 或 podman-compose,并不是内置。
我用一张表总结日常命令对照,适合快速参考:
| 操作 | Docker | Podman |
|---|---|---|
| 拉取镜像 | docker pull | podman pull |
| 查看容器列表 | docker ps | podman ps |
| 运行容器 | docker run | podman run |
| 查看日志 | docker logs | podman logs |
| 停止容器 | docker stop | podman stop |
| 删除容器 | docker rm | podman rm |
| 构建镜像 | docker build | podman build |
| 查看磁盘占用 | docker system df | podman system df |
操作层面基本是无痛迁移,但我建议不要直接在旧生产环境里无脑 alias,而是先在测试环境把镜像跑起来,重点检查网络模式、卷挂载和 SELinux 相关行为。毕竟 Podman 对安全上下文的要求比 Docker 更严格,老镜像一旦遇到权限问题,排查方向会完全不同。我自己就把一套 CI 脚本从 docker 换成了 podman,除了个别依赖 docker-compose 的 job 做了调整,其他基本没动。
4. Rootless 与安全:普通用户也能跑容器,凭什么
4.1 用户命名空间:容器里的 root,不是主机上的 root
Podman 的 rootless 模式是让我最感兴趣的部分。简单说,普通用户启动容器时,Podman 会创建一个新的用户命名空间,在这个命名空间里,当前普通用户被映射成容器内的 UID 0(root)。所以容器进程以为自己以 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-drop=ALL --cap-add=NET_BIND_SERVICE myapp或者用 --security-opt seccomp=unconfined 仅在需要调试时使用。生产环境尽量保持默认,减少不必要的暴露。如果你真的需要一个特权容器,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 后可以直接给集群用,开发、测试、生产之间的差距更小。
实际操作很简单。先创建一个 Pod:
podman 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这时候两个容器共享 localhost,nginx 配置里直接写 proxy_pass http://localhost:8080 就能访问 backend 的服务。如果你一开始没有把端口映射加到 Pod 上,后面再加会比较麻烦,所以建议在创建 Pod 时就把所有需要暴露的端口指定好。
5.2 用 generate kube 和 play kube 连接 Kubernetes
Podman 的 podman generate kube 和 podman play kube 是很有实用价值的两个命令。执行 podman generate kube mypod 会输出一个符合 Kubernetes 语法的 YAML 文件;在本地用 podman play kube mypod.yaml 则可以把这份 YAML 直接在 Podman 环境里跑起来。这就实现了“一份 YAML,K8s 和 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] Description=My web container [Container] Image=docker.io/library/nginx:latest PublishPort=8080:80 Volume=./data:/usr/share/nginx/html:Z [Service] Restart=on-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_start=80,但改宿主内核参数有安全影响,尽量不用。另一个典型坑是 rootless 容器之间的网络互通。
一般建议用 pasta 或 slirp4netns 做用户态网络,老版本默认 slirp4netns,转发性能不理想;新版可以试试 --network=pasta,CPU 开销会低一些。如果追求最大性能,可以让容器以 rootful 模式跑,或者使用 macvlan/host 模式直连宿主机网络,但代价是隔离性减弱。需要在性能和隔离之间做个取舍。实测下来,开发机用 rootless 没问题,但压测环境我会切到 rootful,避免网络转发成为瓶颈。
7.2 挂载目录后权限错乱/读不到文件
Podman rootless 下挂载宿主目录,最常遇到 UID 对不上:容器内进程是映射后的 root,而宿主机目录属于 uid 1000,结果容器写文件很别扭。我的做法是给容器传 --userns=keep-id,让容器内用户保留你的宿主 UID;或者在挂载卷时加 :Z 参数让 SELinux 自动设置标签。注意 :Z 会自动 relabel 目录,如果目录被多个容器共享,可能要用 :z,否则第二次挂载会因标签冲突失败。
如果是线上老镜像,还容易出现“宿主机上明明能读,容器里却 Permission denied”的情况。这时除了看 UID,还要确认 SELinux 拦截日志:
ausearch -m avc -ts recent能看到具体拒绝原因。直接 chmod 777 是治标不治本,还可能弄乱系统安全上下文。根因一般就两个:要么是 UID 映射不对,要么是 SELinux 标签不对。
7.3 alias docker=podman 不是万能的
最后一个排坑点关乎拿来主义。虽然 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 跑一周。一开始可能有些不顺,但适应之后,你会觉得容器本来就该这么用。