Podman Quadlet 完整指南 — 如何使用 systemd 声明式地管理容器

用一行podman run命令启动容器很容易。然而,当您开始添加诸如服务器重启后自动启动、异常终止时重启、执行顺序、日志查询和资源限制等操作问题时,shell 脚本会迅速变得复杂。

Podman Quadlet 弥补了这一差距。通过编写简短的声明式文件,如.container、.network和.volume,systemd 生成器会将其转换为常规的服务单元。此后,容器也可以通过systemctl启动和停止,并通过journalctl进行观察。

总而言之,如果您希望在单台或少数 Linux 服务器上将容器作为操作系统服务运行,Quadlet 是最自然的选择。它不像 Kubernetes 那样在多个节点上调度工作负载,但可以充分利用 systemd 已提供的启动顺序、重启策略、依赖关系、日志和权限模型。

什么是 Quadlet?

Podman 官方文档将 Quadlet 描述为一种以 systemd 单元文件形式声明式管理容器、Pod、卷、网络和镜像的功能。这里的关键词是声明式和systemd。

典型的podman run命令是一个过程:“现在以这些选项运行此容器。” Quadlet 文件记录了所需的状态:“此主机应具有具有此镜像、端口、卷和重启策略的服务。” 实际的执行命令由 Quadlet 生成器创建。

过去的podman generate systemd是基于已创建的容器或 Pod 生成冗长的.service文件的方式。目前,官方命令列表中此功能已标记为已弃用,新配置建议使用 Quadlet。

工作原理:Quadlet 是源,systemd 是执行者

Quadlet 不是一个持续运行的独立守护进程。当 systemd 启动或执行systemctl daemon-reload时,Podman 的生成器会从搜索路径中读取 Quadlet 文件。然后,它在运行时生成 systemd 可理解的服务单元。此过程和搜索路径在podman-systemd.unit 官方文档中定义。

图:Quadlet 生成流程 · 基于Podman systemd unit 官方文档重构

例如,web.container会变成web.service,app.network会变成app-network.service,data.volume会变成data-volume.service。生成的结果是瞬态单元(transient unit),因此不应直接编辑。要更改设置,必须修改原始 Quadlet 或.d/*.conf的 drop-in 文件,然后再次执行daemon-reload。

这种结构使职责明确:

  • 人工管理的源文件:*.container、*.pod、*.network等
  • 转换时机:启动或systemctl daemon-reload
  • 生命周期管理:systemctl start、stop、restart、status
  • 日志和故障分析:journalctl、systemd-analyze、Podman 命令

支持的 Quadlet 文件类型

目前,官方文档定义了以下八种类型:

扩展名 专用部分 作用
— — —
.container [Container] 将单个容器作为服务运行
.pod [Pod] 创建供多个容器参与的 Pod
.network [Network] 创建 Podman 网络
.volume [Volume] 创建/确保持久卷
.image [Image] 预取并缓存镜像
.build [Build] 使用 Containerfile 构建本地镜像
.kube [Kube] 使用podman kube play运行 Kubernetes YAML
.artifact [Artifact] 获取 OCI artifact 供其他单元使用

Quadlet 文件同时使用通用 systemd 语法和专用部分。[Unit]、[Service]和[Install]由 systemd 处理,而[Container]等专用部分由 Podman 生成器转换为podman run选项。因此,您可以在一个文件中组合熟悉的 systemd 策略(如Restart=、TimeoutStartSec=)和容器设置(如Image=、PublishPort=、Volume=)。

文件放在哪里

搜索路径因操作主体是 root 用户还是普通用户而异。

执行方式 常用路径 systemctl 命令
— — —
无根用户服务 ~/.config/containers/systemd/ systemctl --user ...
有根系统服务 /etc/containers/systemd/ sudo systemctl ...
有根临时测试 /run/containers/systemd/ sudo systemctl ...
发行版提供的有根单元 /usr/share/containers/systemd/ sudo systemctl ...
管理员按用户部署 /etc/containers/systemd/users/${UID}/ 该用户的systemctl --user ...

对于无根用户,这还包括$XDG_RUNTIME_DIR/containers/systemd/、$XDG_CONFIG_HOME/containers/systemd/、/etc/containers/systemd/users/和/usr/share/containers/systemd/users/系列。如果多个路径中存在同名文件,则应用优先级,因此应避免操作文件和临时测试文件之间的名称冲突。

Quadlet 要求 cgroup v2。开始前最好通过以下两个命令检查环境:

podman --version
podman info --format '{{.Host.CgroupsVersion}}'

第二个结果应为 v2。此外,由于 Quadlet 是 systemd 功能,它不能直接在 macOS 或 Windows 主机上运行。在这种环境下,应以目标 Linux 服务器或 Podman Machine 内部的 Linux VM 为基准进行设置。

实践:使用 Quadlet 运行无根 Nginx

最安全的起点是具有普通用户权限的无根服务。以下示例仅将 Nginx 暴露在本地地址 127.0.0.1:8080,并将主目录中的静态文件以只读方式挂载。

首先,创建目录和测试页面:

mkdir -p ~/.config/containers/systemd ~/quadlet/site
printf '<h1>Hello from Podman Quadlet</h1>
' > ~/quadlet/site/index.html

创建~/.config/containers/systemd/web.container文件:

[Unit]
Description=Rootless Nginx managed by Podman Quadlet

[Container]
Image=docker.io/library/nginx:alpine
PublishPort=127.0.0.1:8080:80
Volume=%h/quadlet/site:/usr/share/nginx/html:ro,Z
LogDriver=journald

[Service]
Restart=on-failure
TimeoutStartSec=900

[Install]
WantedBy=default.target

各项的含义如下:

  • Image=指定包含注册表的完整镜像名称,而不是短名称。
  • PublishPort=127.0.0.1:8080:80仅绑定到本地主机,而不是所有接口。
  • %h是 systemd 的用户主目录指定符。
  • ro,Z表示只读挂载和 SELinux 私有重新标记。如果多个容器共享同一路径,则应审查z的含义。
  • 首次镜像拉取可能会超过 systemd 的默认启动超时,因此TimeoutStartSec=900留有余量。官方文档也以该设置作为慢速拉取/构建的示例。
  • [Install] WantedBy=default.target使该服务在用户 systemd 启动时处于所需状态。

使文件可读并启动服务:

systemctl --user daemon-reload
systemctl --user start web.service
systemctl --user status web.service --no-pager
curl http://127.0.0.1:8080

日志和容器状态可按如下方式查看:

journalctl --user -u web.service -n 50 --no-pager
podman ps --filter name=systemd-web

如果未单独指定ContainerName=,Quadlet 通常会使用带有systemd-前缀的名称,以减少与用户手动创建的容器的冲突。

为什么不执行 systemctl enable

这里最常犯的错误是对生成的web.service执行systemctl --user enable。Quadlet 创建的服务是瞬态单元,因此不能像普通服务那样启用。相反,生成器会读取原始 Quadlet 的[Install]部分并创建与 enable 相同的链接。

因此,如果需要在启动/登录时自动启动,请在原始文件中添加以下内容,然后执行daemon-reload:

[Install]
WantedBy=default.target

无根用户管理器通常随登录会话一起启动。如果服务需要在登录前启动并在注销后保持运行,则应检查管理策略并使用 systemd lingering。loginctl 官方文档解释说,启用 lingering 会在启动时创建该用户的用户管理器,并在注销后保持运行。

sudo loginctl enable-linger USER_NAME

lingering 允许长期运行的进程在用户未登录时也能保持,因此最好只应用于必要的服务账户。

将网络和卷与单独的 Quadlet 连接

随着规模的扩大,网络和卷也可以分离到声明文件中。如果创建app.network和data.volume并在容器中通过文件名引用它们,Quadlet 不仅会添加 Podman 参数,还会添加生成服务之间的依赖关系。

图:Quadlet 资源引用和依赖关系 · 基于Podman systemd unit 官方文档重构

app.network可以按如下方式最小化创建:

[Network]
Label=app=quadlet-demo

data.volume如果未强制使用单独的名称,Quadlet 会生成一个名称以避免冲突。

[Volume]
Label=app=quadlet-demo

在容器中,引用的是 Quadlet 文件名,而不是资源的实际 Podman 名称。

[Container]
Image=docker.io/library/alpine:latest
Exec=sh -c "while true; do date >> /data/heartbeat.log; sleep 60; done"
Network=app.network
Volume=data.volume:/data

[Service]
Restart=always

[Install]
WantedBy=default.target

如果将此文件保存为worker.container并执行systemctl --user daemon-reload,则会创建worker.service在网络和卷服务之后启动的关系。用户无需手动匹配冗长的Requires=和After=列表是 Quadlet 的一大优势。

以同样的方式,可以引用其他 Quadlet,例如Pod=backend.pod、Image=app.build、Image=base.image。复杂服务之间的执行顺序可以通过[Unit]中的Requires=、After=、Before=明确指定,Quadlet 文件名之间的依赖关系也会转换为生成的服务名称。

安装、修改、删除的操作流程

除了手动复制文件到搜索路径外,支持的 Podman 版本可以使用podman quadlet命令。

podman quadlet install ./web.container
podman quadlet list
podman quadlet print web.container
podman quadlet rm web.container

podman quadlet install会将文件安装到当前用户路径,并默认执行 systemd reload。它还具有将多个 Quadlet 和配置文件作为一个应用程序安装的功能,因此在部署工具中,它比单独复制文件更有利于一致的打包。

对于手动管理,修改顺序很简单:

# 1. 修改原始 web.container
# 2. 更新生成的服务
systemctl --user daemon-reload

# 3. 将新设置应用于正在运行的服务
systemctl --user restart web.service

daemon-reload仅重新生成定义,不会自动替换已运行的容器。要应用更改,需要单独执行restart。

故障分析:从 service not found 开始检查

如果 Quadlet 语法错误或使用了比已安装 Podman 更新的选项,生成器可能无法创建.service。此时,很容易将“Unit web.service not found”误解为镜像问题。

首先,执行官方文档中指导的验证命令:

systemd-analyze --user --generators=true verify web.service

如果想同时查看生成结果和错误,请使用生成器的 dry-run。根据发行版,可执行文件路径可能不同,因此需要确认实际安装路径。

/usr/lib/systemd/system-generators/podman-system-generator --user --dryrun

然后,逐层检查:

# 检查生成的 systemd 定义
systemctl --user cat web.service

# systemd 状态和最新日志
systemctl --user status web.service --no-pager
journalctl --user -u web.service -b --no-pager

# Podman 查看的容器状态
podman ps --all --filter name=systemd-web
podman inspect systemd-web

如果首次运行在 90 秒左右失败,则怀疑是镜像拉取时间问题;如果是端口绑定错误,则检查现有进程或容器是否正在使用相同的端口。在 SELinux 环境中,如果绑定挂载访问被拒绝,与其无条件关闭安全标签,不如先检查:Z或用于共享目的的:z是否正确。

自动更新很方便,但它是一种部署策略

Quadlet 的[Container]部分可以指定AutoUpdate=registry或AutoUpdate=local。Podman auto-update 文档指出,registry策略会检查远程镜像摘要是否已更改,拉取新镜像,然后重启相应的 systemd 服务。此策略需要包含注册表的完整镜像名称。

[Container]
Image=quay.io/example/myapp:stable
AutoUpdate=registry

可以在不实际更改的情况下提前检查更新可用性:

podman auto-update --dry-run

Podman 提供了podman-auto-update.service和计时器,但在启用自动更新之前,需要决定以下事项:

  • 是否可以立即接受可变标签,还是应该通过摘要固定版本?
  • 如果新镜像失败,谁来检测和回滚?
  • 数据迁移和向后兼容性是否得到保证?
  • 私有注册表认证文件的生命周期和权限是否合适?

自动更新可以快速应用安全补丁,但也可能将未经验证的更改自动部署到生产环境。对于关键服务,在 CI 中验证镜像并更新摘要的方式可能更具可预测性。

提高安全性和稳定性的设置

Quadlet 使无根操作变得更容易,但这并不意味着容器会自动安全。镜像、端口、挂载、能力和秘密的边界仍然需要设计。

1. 默认使用无根模式

如果不需要特权端口、设备访问、主机网络等必须 root 才能使用的功能,请从普通用户 Quadlet 开始。在有根 Quadlet 中添加[Service] User=来切换到无根模式是不支持的。无根模式需要使用用户搜索路径和systemctl --user。

2. 明确指定端口暴露范围

PublishPort=8080:80可能会开放到所有 IPv4 接口。对于反向代理后的服务或本地管理 UI,应明确指定绑定地址,例如127.0.0.1:8080:80。

3. 不要将秘密直接写入 Environment=

Quadlet 支持Secret=来连接 Podman secret。密码和令牌不应以明文形式留在声明文件、生成服务和状态输出中,而应通过podman secret create单独管理,并仅传递给必要的容器。

4. 减少写入、权限和资源

如果应用程序允许,请考虑以下选项:

[Container]
NoNewPrivileges=true
DropCapability=all
ReadOnly=true
Memory=512m
PidsLimit=256

应用ReadOnly=true时,应用程序需要写入的路径必须通过卷或 tmpfs 分离。移除所有能力后,只通过AddCapability=添加实际需要的权限。

5. 固定镜像名称和更改策略

在生产环境中,避免使用短镜像名称,并明确指定注册表、仓库、标签或摘要。基于标签的自动更新和基于摘要的可重现性是不同的操作策略,因此不要在一个文件中随意混用。

Quadlet、Compose、Kubernetes 中如何选择?

方式 适用场景 优点 局限性
— — — —
podman run·Shell 脚本 一次性测试、调试 快速直接 重启、依赖、状态管理需手动
Podman Quadlet 单台 Linux 主机的长期运行服务 systemd 集成、无根、声明式文件、journal 非多节点调度器
Compose 开发/测试用的多容器组合、Compose 生态系统 在一个文件中表达服务关系,熟悉度高 主机启动·systemd 运行需单独集成
Kubernetes 多节点、复制、滚动部署、服务发现 集群级自动化和生态系统 构建·运行复杂度和成本高

Quadlet 不是“小型 Kubernetes”。它不会在节点故障时将工作负载重新调度到其他服务器,也不会在整个集群中保持多个副本的期望状态。相反,它允许在 systemd 已成为标准的 Linux 主机上,以与其他操作系统服务相同的方式运行容器。

它特别适用于家庭实验室、边缘设备、构建服务器、内部工具、单虚拟机上的 Web 服务以及部署 Kubernetes 过于繁重的小规模运营环境。

六个常见错误

  1. 编辑生成的.service。 它将在下次daemon-reload时重新生成,因此必须修改原始 Quadlet 或 drop-in 文件。
  2. 执行systemctl enable。 将WantedBy=放在原始 Quadlet 的[Install]部分。
  3. 将无根文件放在/etc/systemd/system中。 使用~/.config/containers/systemd/和systemctl --user。
  4. 直接使用短镜像名称和开放端口。 包含注册表并仅绑定到必要的地址。
  5. 认为daemon-reload足以应用更改。 正在运行的服务需要单独执行restart。
  6. 期望 Quadlet 成为集群编排器。 如果需要跨主机调度和高可用性,则需要 Kubernetes 等其他层。

总结

Podman Quadlet 的核心在于不将容器视为 systemd 之外的特殊对象。由人工管理的简短声明文件由生成器转换为服务,systemd 负责启动、顺序、重启和日志,Podman 执行容器、网络和卷。

首先从一个web.container开始。然后,将网络和卷分离到单独的 Quadlet 中,并完善无根操作、端口范围、秘密和资源限制。如果您希望在单个 Linux 主机上创建可重现的容器服务,同时不显著增加操作复杂性,那么 Quadlet 是一个非常实用的起点。

官方参考资料


Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注