用一行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 过于繁重的小规模运营环境。
六个常见错误
- 编辑生成的
.service。 它将在下次daemon-reload时重新生成,因此必须修改原始 Quadlet 或 drop-in 文件。 - 执行
systemctl enable。 将WantedBy=放在原始 Quadlet 的[Install]部分。 - 将无根文件放在
/etc/systemd/system中。 使用~/.config/containers/systemd/和systemctl --user。 - 直接使用短镜像名称和开放端口。 包含注册表并仅绑定到必要的地址。
- 认为
daemon-reload足以应用更改。 正在运行的服务需要单独执行restart。 - 期望 Quadlet 成为集群编排器。 如果需要跨主机调度和高可用性,则需要 Kubernetes 等其他层。
总结
Podman Quadlet 的核心在于不将容器视为 systemd 之外的特殊对象。由人工管理的简短声明文件由生成器转换为服务,systemd 负责启动、顺序、重启和日志,Podman 执行容器、网络和卷。
首先从一个web.container开始。然后,将网络和卷分离到单独的 Quadlet 中,并完善无根操作、端口范围、秘密和资源限制。如果您希望在单个 Linux 主机上创建可重现的容器服务,同时不显著增加操作复杂性,那么 Quadlet 是一个非常实用的起点。
发表回复