OFFSET偏移
← 文章目录OFFSET / FIELD NOTESNO. 012

Quadlet 如何把容器声明变成服务?

走完源文件、systemd 生成、启动验证、持久数据与就绪判断,再定位服务失败。

所在专题容器的权限与生命周期 ↗Podman 容器工作流 ↗
约 12 分钟阅读开始阅读

Quadlet 把容器参数放进资源描述文件,由 systemd generator 生成服务。分清“源文件 → 生成 unit → 运行容器”,才能判断改动在哪一步生效,以及为什么文件已经写好,服务却仍不存在。

Quadlet 描述资源,systemd 管运行过程#

本文基于 2026 年 10 月核验的 stable 文档与 v6.1.3 源码,前提是 Linux、本地 rootless Podman、systemd 用户管理器及 cgroup v2。Podman、生成器和网络工具应已配置;NixOS 继续通过 Nix 声明安装。示例未经本机部署或运行验证。

.container 中的 [Container] 由 Quadlet 转换,常见 [Unit]、[Service] 交给 systemd 处理。web-demo.container 通常生成 web-demo.service,默认容器名是 systemd-web-demo,ContainerName 可以显式改名。服务名和容器名属于不同入口:systemctl 接受前者,podman logs 接受后者。[1]

generator 在管理器启动和 daemon-reload 时读取源文件、生成运行时 unit;daemon-reload 不会因此启动新服务,也不会重新创建已运行容器。生成文件是产物,应修改 .container 源或其 drop-in,而不是手改生成的 .service。

三个文件组成一个完整的本机服务#

以公共 BusyBox 镜像提供静态页面:web-demo.container 引用一个命名卷和一个 bridge 网络。文件放在 $XDG_CONFIG_HOME/containers/systemd/;未设置该变量时使用 ~/.config/containers/systemd/。以下名称仅供演示,先确认没有同名资源。

# web-data.volume
[Volume]
VolumeName=web-demo-data

# web-net.network
[Network]
NetworkName=web-demo-net
Driver=bridge

# web-demo.container
[Unit]
Description=Local Quadlet Web Demo

[Container]
Image=docker.io/library/busybox:1.37
ContainerName=quadlet-web-demo
Network=web-net.network
Volume=web-data.volume:/www
PublishPort=127.0.0.1:8080:8080
Exec=sh -c 'test -e /www/index.html || printf "Quadlet demo\n" > /www/index.html; exec httpd -f -p 8080 -h /www'
LogDriver=journald

[Service]
Restart=on-failure
RestartSec=5
TimeoutStartSec=300

[Install]
WantedBy=default.target

代码块表示三个独立文件,不应把它们合进同一个 .container。卷保存 /www;首次启动仅在页面不存在时写入示例内容。httpd 前台运行,由 systemd 管理生命周期;宿主只发布 127.0.0.1:8080。这里固定的是标签而非不可变摘要,长期运行前应记录实际镜像 digest。

Volume=web-data.volume:/www 与 Network=web-net.network 引用的是 Quadlet 源文件名。转换器会解析实际资源名,并添加相应服务依赖;卷 unit 通常叫 web-data-volume.service,网络 unit 通常叫 web-net-network.service。它们与 VolumeName、NetworkName 分别控制的 Podman 资源名不同。[2][4][5]

生成、检查、启动是三个动作#

保存文件后,先加载和看产物。重载和读取产物不会启动容器;start 才可能拉镜像、创建资源并运行。

systemctl --user daemon-reload
systemctl --user cat web-demo.service

# 明确会启动服务
systemctl --user start web-demo.service
systemctl --user status web-demo.service
journalctl --user -u web-demo.service -n 80 --no-pager

# 服务运行后,从宿主验证 HTTP 行为
curl --fail --show-error http://127.0.0.1:8080/

cat 检查转换参数,status 观察服务状态,curl 验证 HTTP 行为。TimeoutStartSec=300 延长拉镜像或初始化的等待时间,不能修复镜像或网络错误。

不要对生成的 web-demo.service 再执行 systemctl enable。生成服务不能按普通持久 unit 那样 enable;Quadlet 在生成时处理源文件的 [Install],建立 WantedBy 等关联。default.target 关联用户管理器的目标,已运行的目标不会因为一次 daemon-reload 就自动重新启动新服务,所以首次启用仍需显式 start。[1]

重启容器时,数据必须有自己的位置#

v6.1.3 转换器生成 podman run 及退出清理逻辑,停止服务时会清理相应容器。不能依赖容器可写层跨服务重启保留数据。命名卷与 bind mount 的生命周期独立,示例页面保存在 web-demo-data。[2]

# 读取已有资源
podman ps -a --filter name=quadlet-web-demo
podman volume inspect web-demo-data
podman logs --tail 40 quadlet-web-demo

# 这两条会改变服务运行状态
systemctl --user stop web-demo.service
systemctl --user start web-demo.service

修改源文件后,daemon-reload 再 restart 才会更新运行参数。服务停止不等于删除数据卷;删除 .volume 文件也不等于有意识地销毁数据。撤销服务时先 stop、移除或调整源文件与 [Install]、再 daemon-reload,最后分别盘点保留的卷和网络。数据清理要按已确认资源执行,不能顺手全局 prune。

若改为 Volume=%h/path/to/site:/www:ro,文件读写权限仍服从 rootless UID 映射与宿主目录权限。此时 Exec 应改为直接启动 httpd。SELinux 的 Z 只用于专用目录,不能对整个 home 添加。

依赖顺序与应用就绪不是一回事#

After=db.service 只约束相关作业的先后,不会主动拉起 db;Requires=db.service 再加 After 才把拉起关系和顺序组合起来。即便依赖服务达到 active,也不代表数据库完成迁移、监听端口或可以接受请求。业务还需重试连接,或者设计实际健康状态参与就绪判断。

默认 .container 采用 notify 类型,v6.1.3 默认通知来自 conmon。Notify=true 适用于应用自己正确发送 sd_notify;Notify=healthy 则等 Podman 健康检查通过后通知 systemd,需要同时提供有效 HealthCmd。不能给任何镜像机械加 Notify=true,否则可能一直等待从未发生的通知。[3][2]

健康检查命令在容器内部执行,必须有对应工具,且实际代表应用可用。unhealthy 不自动等于进程退出;Restart=on-failure 依据服务失败。若要据持续 unhealthy 重启,应另行设计 HealthOnFailure 策略。

登录、开机与镜像更新各有条件#

WantedBy=default.target 表示用户管理器启动目标时的关联;通常登录会带起用户管理器。若要求未登录也启动、退出登录后继续运行,则需要用户 lingering 政策。可先只查询:

loginctl show-user "$USER" -p Linger

是否启用 linger 是系统管理决定;NixOS 应通过系统配置表达。用户 unit 不能靠在系统服务中写 User=某人就自动变为 rootless Quadlet,应放到对应用户搜索路径并由其用户管理器运行。[1][7]

AutoUpdate=registry 只设置更新标签,还需 podman-auto-update 的服务、timer 或手动调用驱动检查、拉取和服务重启。先安排完整镜像名、更新窗口与数据备份。本例不启用自动更新;也可以固定摘要,再计划修改 Image。[6]

找不到服务时,先检查生成阶段#

Unit not found 首先意味着管理器没有找到服务,可能是搜索路径、语法、缺少被引用文件,或所用字段超出已安装版本。它还没有进入容器应用运行阶段。使用发行版实际提供的 generator 路径做 dryrun:

# 路径示意;NixOS 应查实际安装的生成器,不照抄 /usr/lib 路径
/path/to/podman-system-generator --user --dryrun

# 只检查专用测试目录中的源文件
QUADLET_UNIT_DIRS=/path/to/quadlet-test \
  /path/to/podman-system-generator --user --dryrun

# systemd 版本支持该选项时,验证生成服务
systemd-analyze --user --generators=true verify web-demo.service

dryrun 输出翻译结果和生成错误,不启动服务。出现未知 key 时先对照 podman version 与该版本手册,不要用最新版网页反推旧安装必然支持。服务能生成却启动失败,再看 journal、镜像名称、网络组件和存储权限;active 但 HTTP 失败,继续查端口与应用日志。[1]

源文件、生成 unit 和请求结果分别说明意图、参数与实际行为。

参考与延伸

  1. Quadlet 的生成、搜索路径与启用规则 ↗
  2. Podman 6.1.3 Quadlet 转换源码 ↗
  3. Quadlet container 字段与通知机制 ↗
  4. Quadlet volume 字段 ↗
  5. Quadlet network 字段 ↗
  6. Podman 自动更新 ↗
  7. Podman 官方 rootless 服务排障 ↗

根据技术笔记重新整理,示例使用通用名称。

沿专题继续阅读

容器的权限与生命周期

  1. 01Rootless 容器,文件权限到底算谁的?
  2. 02Quadlet 如何把容器声明变成服务?

Podman 容器工作流

  1. 01Podman 基础原理
  2. 02Podman 基本使用指南
  3. 03Rootless 容器,文件权限到底算谁的?
  4. 04Rootless 配置:映射、ICMP 与注册表
  5. 05Pod 体系与 Kubernetes 清单
  6. 06Quadlet 如何把容器声明变成服务?
  7. 07Quadlet 的 Pod 与多服务配置
  8. 08Compose provider 与 Docker 工具互操作
  9. 09NixOS 上的 Podman 管理
  10. 10Podman 高级运维与调优
  11. 11Docker 与 Podman:架构、兼容与共存
CONTINUE READING / 下一篇NixOS:声明式系统的基础

从配置、构建结果和代际理解 NixOS 的基本工作方式。