Rocky 10:quadlet 与 systemd 的契约,以及离线怎么备镜像
Rocky Linux 10.2 Server 组虚拟机,podman 5.8.2 / quadlet 5.8.2。
实验网访问不到任何 registry,所以先解决「镜像从哪来」,再做 quadlet 全生命周期验证。截图:public/evidence/quadlet/rocky-quadlet.png。
离线备镜像:import 一个最小 rootfs
Section titled “离线备镜像:import 一个最小 rootfs”D=/tmp/lab-img2; mkdir -p "$D/bin" "$D/lib64"cp /bin/bash "$D/bin/bash"for lib in $(ldd /bin/bash | awk '/=>/ {print $3}'); do cp -L "$lib" "$D/lib64/"; donecp -L /lib64/ld-linux-x86-64.so.2 "$D/lib64/"tar -C "$D" -cf /tmp/rootfs.tar .podman import --change 'CMD ["/bin/bash","-c","while true; do read -t 30 || true; done"]' \ /tmp/rootfs.tar localhost/labshell:1read -t 30 是 bash 内建,不需要 coreutils——所以在「只有 bash + 动态库」的极小镜像里也能常驻。
顺带一个真实的坑:从别的架构导出的镜像(arm64)在这台 x86_64 上会 Exec format error,日志写法是 image platform (linux/arm64/v8) does not match the expected platform (linux/amd64)。
契约:生成单元里多出来的字段
Section titled “契约:生成单元里多出来的字段”.container 文件只写了 [Container] 与 [Install],但生成出来的单元还有:
$ systemctl cat webapp.service | grep -E "^(Wants|After|RequiresMountsFor|Delegate|KillMode|ExecStop)"Wants=network-online.targetAfter=network-online.targetRequiresMountsFor=%t/containersKillMode=mixedExecStop=/usr/bin/podman rm -v -f -i lab-webappDelegate=yes每一项都有原因:
| 字段 | 作用 |
|---|---|
Wants/After=network-online.target | 拉镜像前网络就绪 |
RequiresMountsFor=%t/containers | 容器存储挂载完成再启动 |
KillMode=mixed | 先给主进程发信号,再清理整个 cgroup |
ExecStop=podman rm -v -f -i | 停服务即删容器(含匿名卷) |
Delegate=yes | 让容器自己管 cgroup(容器内的 systemd、CPU 限制) |
端口为什么是 conmon 在听
Section titled “端口为什么是 conmon 在听”$ systemctl start webapp.service; sleep 3$ systemctl is-active webapp.serviceactive$ podman ps --format '{{.Names}} | {{.Status}} | {{.Ports}}'lab-webapp | Up 3 seconds | 0.0.0.0:8088->80/tcp$ ss -tlnp | grep 8088LISTEN 0 4096 0.0.0.0:8088 0.0.0.0:* users:(("conmon",pid=10657,fd=5))conmon 是 podman 的容器守护进程,它持有发布端口并转发进容器网络命名空间。所以:
- 用
ss看到的是conmon,不是你的应用进程; - 防火墙放行的也是这个宿主端口;
- 应用没起来时端口照样在 LISTEN(连接会被拒或挂起),别拿「端口在听」当「服务正常」。

daemon-reload 才是生效开关
Section titled “daemon-reload 才是生效开关”quadlet 是 systemd 生成器(/usr/lib/systemd/system-generators/podman-system-generator),只在 daemon-reload 或开机时跑:
$ systemctl daemon-reload # 重新生成 webapp.service$ systemctl start webapp.service改了 .container 不 reload,systemctl cat 里还是旧内容——这是最常见的「配置不生效」。
排查三条命令
Section titled “排查三条命令”systemctl cat webapp.service # 生成后的真实单元(唯一可信来源)journalctl -u webapp.service -n 50 # podman 的拉取/启动/退出原因都在这podman ps -a --format '{{.Names}} {{.Status}}' # 残留容器与崩溃循环systemctl cat 特别重要:手写的 .container 与最终 podman run 参数之间隔着一层生成器,猜不如看。
Exec format error:镜像架构与本机不符(我们用 arm64 镜像踩过);- 服务反复重启:
Restart=on-failure会把失败循环放大,先看journalctl里的进程退出码; podman ps为空但端口被占:可能是上一个conmon没退,podman ps -a与ss一起看;- 不要用
WantedBy=multi-user.target之外的花样:quadlet 默认期望default.target。
练习:从失败到成功走一遍
Section titled “练习:从失败到成功走一遍”sudo tee /etc/containers/systemd/broken.container >/dev/null <<'EOF'[Container]Image=localhost/missing:1[Install]WantedBy=default.targetEOFsudo systemctl daemon-reloadsudo systemctl start broken.service # 预期失败sudo journalctl -u broken.service -n 10 --no-pager | tail -5检查:
- 失败原因是镜像不存在(而不是语法错误);
- 把
Image=改成localhost/labshell:1后daemon-reload+start成功; systemctl cat里能看到对应的podman run;- 收尾
systemctl disable --now并删除文件、再 reload。
常见问题
离线环境怎么给 quadlet 供镜像?
在有网的机器上导出(skopeo/podman save),拷进来 podman load;或者像本文一样用 podman import 做最小镜像。
容器里的服务怎么写日志?
直接写 stdout/stderr,podman 会转发到 journald,用 journalctl -u <unit> 查看。
能不能让容器在失败时自动重建?
能,[Service] 的 Restart=on-failure 配合 RestartSec;注意失败循环会刷日志,先定位根因。
- 容器与镜像:/containers/
- 日志:/journald-logging/
- 服务与定时器:/systemd-timers/