Podman 入门实战:无守护进程容器与 systemd 托管的正确姿势

从 Docker 切换到 Podman,最大的不适应不是命令,而是模型:Docker 背后有常驻的 dockerd 统一管理容器,Podman 没有这个进程,容器就是普通子进程,由 systemd 或用户会话直接拉起。理解这一点,它为什么适合 systemd 托管、为什么 rootless 是默认姿势就顺理成章了。本文以 Rocky Linux 9 / Podman 4.x 为口径,讲清模型差异、命令对照与 Quadlet 写法,并给出一份迁移检查清单。

前置条件

  • 一台 Rocky Linux 9 或同系发行版主机(Ubuntu 22.04+ 也可以,安装命令略有差异,文中会注明)。
  • 一个普通 sudo 用户,后续 rootless 实验都以该用户身份操作。
  • 主机已启用 cgroup v2(Rocky 9 默认),可用 stat -fc %T /sys/fs/cgroup/ 确认输出为 cgroup2fs。
  • 从属 UID/GID 已分配(通常自动写入 /etc/subuid,如无可手动补)。

先搞清楚:与 Docker 的模型差异

无守护进程(daemonless)。dockerd 是 Docker 的中心:CLI 把请求发给守护进程代为操作容器,它挂掉或升级重启管理面都会受影响,还必须以 root 长期运行。Podman 采用 fork/exec 模型,podman run 通过 conmon 直接拉起容器,命令退出后容器照常运行,不存在单点管理进程。

rootless 是一等公民。借助用户命名空间(user namespace),普通用户在自己的命名空间里“扮演 root”,实际映射到宿主机上一段无人使用的从属 UID(/etc/subuid 分配),进程逃逸也只拿到宿主机普通用户权限。代价是网络走用户态转发(新版本默认 pasta,旧版 slirp4netns),性能略低于 rootful 桥接,绑定低位端口、挂载设备也会受限,常见问题里会交代。

与 systemd 同源协作。容器本质是普通进程,Podman 通过 sd_notify 向 systemd 汇报就绪,cgroup 挂在单元之下,日志直接进 journald。再进一步是 Quadlet:把容器写成 .container 声明文件,生成器自动翻译成 service 单元。早期 podman generate systemd 已弃用,新部署一律 Quadlet。

第一步:安装与基本命令对照

Rocky 系用 dnf 安装,Debian 系用 apt 安装 podman 包。装完先看运行时信息,确认当前是 rootless 还是 rootful。

sudo dnf install -y podman
podman info --format '{{.Host.Security.Rootless}} {{.Version.Version}}'
podman run --rm docker.io/library/alpine echo hello

日常命令与 Docker 几乎一一对应:pull / build / run / ps / images / exec / logs / stop / rm / volume / network 参数都兼容,肌肉记忆可直接平移;脚本里写死了 docker 的,可装 podman-docker 包获得同名软链接。真正的差异主要在这几处:

  • 镜像短名:未写全的镜像名会提示选仓库或报错,要写全 docker.io/library/nginx 这样的全名,或在 registries.conf 里配置 unqualified-search-registries。
  • Pod 与 K8s 清单互通:多了 podman pod 命令组管理 Pod(同 Pod 内容器共享网络命名空间);podman play kube 能按精简版 K8s YAML 拉起 Pod,generate kube 反向导出。
  • Compose 与 API:4.4 起自带 podman compose(背后调用 docker-compose 或 podman-compose),复杂文件迁移前逐服务验证;需要 Docker 兼容 API 时启动 podman.socket 用户服务,把 DOCKER_HOST 指向其 socket。

第二步:用 Quadlet 把容器交给 systemd

把声明文件放到固定目录:rootful 放 /etc/containers/systemd/,rootless 放 ~/.config/containers/systemd/。文件名即单元名,web.container 会生成 web.service。

[Unit]
Description=Nginx demo container
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/library/nginx:1.27-alpine
ContainerName=web
PublishPort=8080:80
Volume=/srv/web/html:/usr/share/nginx/html:Z,ro
Environment=TZ=Asia/Shanghai
Label=app=web

[Service]
Restart=always
TimeoutStartSec=120

[Install]
WantedBy=default.target

逐项说明几个关键字段:

  • Image:镜像全名,固定到具体版本标签,别用 latest,升级时改这一行再 reload。
  • PublishPort:端口映射,写法同 -p;rootless 下映射 1024 以下端口默认不行,处理见常见问题。
  • Volume:路径挂载。末尾 :Z 是 SELinux 独占 relabel,ro 只读,共享用 :z;rootless 下还要关注属主映射,见迁移清单。
  • Environment / EnvironmentFile:注入环境变量,敏感值走 EnvironmentFile 并把权限收到 600。
  • Network:指定自定义网络,不写用默认;容器间互通先 podman network create 建网,容器名即 DNS 名。
  • [Service] Restart:重启策略在 [Service] 段而非 [Container] 段,最常见的写错位置。
  • [Install] WantedBy:自启挂载点,rootless 用 default.target,rootful 用 multi-user.target。

写好后执行 systemctl --user daemon-reload(rootful 去掉 --user),生成器把 .container 翻译成单元,之后就能像管理普通服务一样管理容器:

systemctl --user daemon-reload
systemctl --user enable --now web.service
systemctl --user status web.service
journalctl --user -u web.service -f

日志默认进 journald:journalctl -u 按单元看,podman logs web 同样可读,排障优先前者,它能把启停事件与容器输出按时间对齐。自启前提:rootless 单元挂在用户会话下,注销后默认被清理,需 loginctl enable-linger $USER 让用户级 systemd 常驻。

第三步:从 Docker 迁移的检查清单

  • 卷权限与 SELinux:rootless 下容器内 root 映射为宿主机普通用户,挂载目录读写失败是最常见的迁移故障。先用卷选项 :U 让 Podman 自动修正属主;还报 Permission denied 就怀疑标签,再补 :Z(独占)或 :z(共享)。不要无脑 chmod 777 或关 SELinux。
  • 特权与设备:--privileged 在 rootless 下语义完全不同,靠特权的操作(挂载、改内核参数)多半做不到,这类负载考虑 rootful 或拆分权限。
  • 网络模型:4.x 默认后端 netavark(旧版 CNI),DNS 行为与 Docker bridge 略有差异;pasta 对源 IP 的保留与 rootful 不同,依赖真实源 IP 的服务要实测。
  • 工具链变量:CI 里写死 docker.sock 的地方,改成 podman 的用户 socket 或 rootful 的 /run/podman/podman.sock。

验证

  • podman ps 里 web 为 Up,curl http://127.0.0.1:8080 能返回页面。
  • systemctl --user is-enabled web.service 返回 enabled,restart 后容器自动恢复。
  • journalctl --user -u web.service 能看到完整启动日志。

常见问题

rootless 下映射 80 端口报 permission denied。普通用户默认不能绑定 1024 以下端口。临时验证先用高位端口;正式处理是调低阈值:sudo sysctl net.ipv4.ip_unprivileged_port_start=80 并写入 /etc/sysctl.d/ 持久化,或把服务放到反向代理之后。不要为省事直接改 rootful,先评估对同机其他服务的影响。

Quadlet 改了文件却不生效。按顺序排查:文件放对目录(rootless 是 ~/.config/containers/systemd/,放错一层不被扫描);改完必须 systemctl --user daemon-reload,生成器只在 reload 时工作;用 quadlet --dryrun(位于 /usr/libexec/podman/ 下)看解析错误;启停操作生成的 .service,不是 .container 本身。

挂载目录在容器里读不到或写不了。先查属主映射:宿主机目录属于谁、容器进程以哪个映射 UID 访问。用命名卷或给 Volume 加 :U 多数能解决,叠加 SELinux 时再补 :Z。两者常同时出现,逐个排除,别一次改一堆参数。

用户退出登录后容器全没了。没开 linger 的典型症状,执行 loginctl enable-linger $USER 后重新 enable 单元即可。

小结

Podman 的要点就三句:没有守护进程,容器就是进程,systemd 是它天然的管家;rootless 是默认推荐姿势,权限与网络的限制要提前算账;托管用 Quadlet 写声明,别再用已弃用的 generate systemd。从 Docker 迁过来命令几乎不用重学,真正要逐项过的是卷权限、SELinux 标签、端口与 socket 路径。

发表回复

后才能评论