Article

用 Podman 和 Quadlet 管理家庭服务器容器

前言

6 月份折腾家里那台 Linux 服务器的时候,我把容器环境换成了 rootless Podman,并且用 Quadlet 把 Caddy、Gitea、Nextcloud 这些容器统一交给 systemd 管理。

不得不说 Quadlet 写起来确实比以前手写 .service 清爽多了,但刚开始上手时我也踩了不少外围的坑:比如 .container 和 .target 到底该放哪个文件夹?为什么 systemctl enable 会报错?多个容器怎么打包一起启停?挂载目录报 permission denied 又是怎么回事?

这篇笔记把当时折腾的过程和常用命令汇总一下,免得隔段时间没登服务器又忘光了。下面默认都是以 **Linux 普通用户下的 rootless Podman(带 --user)**为例。

Quadlet 与 systemd 的工作流程

刚用 Quadlet 时最容易搞混的一点,就是不知道该改哪个文件、启动哪个服务。其实它们的关系很直接:

text
caddy.container       我们手写的容器配置
        ↓ 执行 daemon-reload 时由 Quadlet 自动转换
caddy.service         自动生成的 systemd 服务单元
        ↓ 由 systemd 调用
podman run …          实际拉起容器

平时维护只要记住一条原则:改配置去编辑 .container 文件,启停服务去操作 .service 单元。每次系统启动或者跑 systemctl --user daemon-reload 时,Quadlet 都会重新读取 .container 覆盖生成 .service,所以千万不要直接去改生成的 .service 文件(参考 Quadlet 官方文档)。

至于看容器状态、查镜像和网络,还是照常敲 Podman 命令:

bash
podman ps -a
podman images
podman network ls

注意

注意别混用 sudo 跑 rootless 容器时,上面这些命令直接用普通用户敲就行。如果顺手加了 sudo podman ps,查的是 root 用户下的容器,自然看不到普通用户跑起来的服务。

配置文件存放路径

当时我折腾的时候就在想:所有的配置文件能不能图省事全塞在 ~/.config/systemd/ 底下?答案是不行,因为读取它们的程序不一样:

配置文件类型普通用户(rootless)存放位置系统级(root)存放位置
Quadlet 容器与网络(.container / .network)~/.config/containers/systemd//etc/containers/systemd/
手写的原生 systemd 单元(如 .target)~/.config/systemd/user//etc/systemd/system/

比如我的服务器上跑着 Caddy、Gitea 和 Nextcloud,还想建一个 app.target 把后面两个应用编成一组,目录结构就是这样的(参考 Quadlet 搜索路径文档):

text
~/.config/containers/systemd/
├── caddy.container
├── app-gitea.container
└── app-nextcloud.container

~/.config/systemd/user/
└── app.target

编写与启动容器单元

可以先拿一个最简单的 Alpine 容器练练手。在 ~/.config/containers/systemd/quadlet-demo.container 里写入:

ini
[Unit]
Description=Quadlet demo

[Container]
Image=docker.io/library/alpine:3.22
ContainerName=quadlet-demo
Exec=sleep infinity

[Service]
Restart=on-failure
TimeoutStartSec=900

这里文件名 quadlet-demo.container 决定了生成的服务名叫 quadlet-demo.service;而 ContainerName=quadlet-demo 则是 podman ps 里显示的容器名。因为第一次启动可能要拉取镜像,所以把 TimeoutStartSec 设长一点,防止网速慢被 systemd 判定为启动超时(详见 容器单元配置说明)。

保存之后重载配置并启动:

bash
systemctl --user daemon-reload
systemctl --user start quadlet-demo.service
systemctl --user status quadlet-demo.service --no-pager

平时想手动停掉或者重启,直接操作 .service:

bash
systemctl --user stop quadlet-demo.service
systemctl --user start quadlet-demo.service
systemctl --user restart quadlet-demo.service

只要改过了 .container 文件,一定要先跑一次 systemctl --user daemon-reload,再 restart 服务,新配置才会在重建容器时生效。

配置容器开机自启

这是我最开始踩的一个坑:按照以前用 systemd 的习惯,写完配置我就去敲 systemctl --user enable xxx.container 或者 enable xxx.service,结果报错提示无法 enable。

因为 Quadlet 生成的单元本来就是动态生成的,它的自启动关系不能靠手动敲 enable 命令,而是直接在 .container 文件末尾加上 [Install] 段(参考 Quadlet 启用说明):

ini
[Install]
WantedBy=default.target

写上这两行之后,只要执行一次 systemctl --user daemon-reload,Quadlet 生成器就会自动把它挂到 default.target 底下实现自启。

提示

自启动与手动启停的关系 当然可以。WantedBy=default.target 只是管开机(或用户管理器启动)时拉起它,平时随时可以用 systemctl --user stop 把它停掉。如果以后都不想让它开机自启了,把 [Install] 这两行删掉再 daemon-reload 就行。

另外,因为我们用的是普通用户的 --user 服务,默认情况下只有用户登录了 SSH,用户的 systemd 实例才会启动;一旦退出 SSH,服务就可能被系统停掉。为了让服务器开机就自动在后台跑普通用户的容器,需要给当前用户开启 linger(参考 loginctl 文档):

bash
sudo loginctl enable-linger penry
loginctl show-user penry -p Linger

使用 target 分组管理容器

当我想把 Gitea 和 Nextcloud 放在一组管理时,就在 ~/.config/systemd/user/app.target 里建了一个手写的 target 文件:

ini
[Unit]
Description=Application Stack
Wants=app-nextcloud.service app-gitea.service

[Install]
WantedBy=default.target

因为这个 .target 文件是我们自己手写的普通 systemd 单元,不是 Quadlet 生成的,所以它需要像普通服务一样补上 [Install] 段,并用 enable 启用(我最开始忘了写 [Install],结果 enable app.target 直接报了 The unit files have no installation config):

bash
systemctl --user daemon-reload
systemctl --user enable --now app.target
systemctl --user list-dependencies app.target

这里有三个很容易混的指令:

  1. Wants=:写在 app.target 里,意思是启动 app.target 时顺带拉起这两个服务(既然由 app.target 拉起了,那两个容器自己的 .container 里就不用再重复写 WantedBy=default.target 了)。
  2. After=:Wants= 不管先后顺序,如果必须先起某个容器,要另外加 After=。
  3. PartOf=:如果只在 app.target 里写了 Wants=,当你敲 systemctl --user stop app.target 时,那两个容器是不会跟着停掉的!要想让停止和重启操作也能同步传给组里的每个容器,需要在每个容器的 .container 文件 [Unit] 段里加上(参考 systemd Unit 文档):
ini
[Unit]
PartOf=app.target

Caddy 容器启动失败排查

6 月中旬配 Caddy 容器(用的是本地构建的 localhost/caddy-alidns:latest 镜像)的时候,服务一启动就挂了,敲 status 只看到下面两行:

text
Active: failed (Result: exit-code)
Start request repeated too quickly

看到 Start request repeated too quickly 别慌,这只是说明容器崩得太快、连续重启了好几次被 systemd 拦住了,真正的报错藏在前面的日志里。可以按顺序敲这几条命令查原因:

bash
# 看详细的报错日志
journalctl --user -u caddy.service -b -n 100 --no-pager

# 看 Quadlet 最终转换出来的 service 内容和实际运行的 podman 命令
systemctl --user cat caddy.service
systemctl --user show caddy.service -p ExecStart

# 检查镜像和网络在不在
podman image inspect localhost/caddy-alidns:latest
podman network inspect caddy

如果连 caddy.service 都提示找不到,说明 .container 文件写错了或者放错了目录,可以直接跑一下 Quadlet 生成器的检查命令看它在哪一行报错(参考 Quadlet 调试文档):

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

那次排查 Caddy 遇到的两个具体问题也很典型:

1. 端口映射与监听端口配置

日志里既报过 :80 端口绑定失败(rootless 默认不能直接绑 1024 以下的低端口),又看到 .container 里配的是 PublishPort=3000:3000。要注意:只改 .container 里的 PublishPort 是不会自动帮你改容器内 Caddyfile 配置的,宿主机放行的端口、Podman 转发的端口和 Caddy 实际监听的端口得对得上才行。

改好配置后,先清掉 systemd 的失败计数再重启:

bash
systemctl --user reset-failed caddy.service
systemctl --user restart caddy.service

2. 挂载目录权限与 UID 映射

端口通了之后,Caddy 读写挂载目录(/data/containers/penry/caddy/data 和 config)时又报了权限错误:

text
open /config/caddy/autosave.json: permission denied
open /data/caddy/instance.uuid: permission denied

在 .container 里经常会看到这四个跟权限和镜像相关的参数(参考 User 与命名空间文档):

ini
[Container]
Pull=newer
UserNS=keep-id
User=1000:1000
SecurityLabelDisable=true

别把它们当成万能模板到处贴,先搞清楚各自是干嘛的:

  • UserNS=keep-id:把宿主机当前用户(比如 UID 1000)直接映射成容器里的 1000。
  • User=1000:1000:强制指定容器内进程用 1000 跑(注意有些镜像启动时必须先用 root 初始化再自己降权,硬加这行反而起不来)。
  • SecurityLabelDisable=true:关掉这个容器的 SELinux 隔离。其实如果系统开了 SELinux,在挂载路径末尾加上 :Z(当前容器私有)或 :z(多容器共享)让 Podman 自动打标签会更好(详见 Podman 挂载选项)。

遇到目录没权限时,先看一眼宿主机目录的数字属主和容器命名空间映射(参考 podman unshare 文档):

bash
id
ls -ldn /data/containers/penry/caddy/data
ls -ldn /data/containers/penry/caddy/config
podman unshare cat /proc/self/uid_map /proc/self/gid_map

镜像拉取策略与自动更新

最后记一下容器镜像更新的两种方式:

  • Pull=newer:只在你重启或重建容器的时候去远端比对一下镜像 digest,平时容器跑着的时候它是不会自己去检查更新的(参考 镜像拉取策略)。
  • AutoUpdate=registry:配合 systemd 定时器自动去远端仓库查更新,发现新镜像就拉下来并重启服务(需要镜像名写完整的远端地址,像本地自己 build 的 localhost/caddy-alidns:latest 不能用 registry 模式):
ini
[Container]
AutoUpdate=registry

如果想开启定时自动更新,启动对应的用户级 timer 即可(详见 podman-auto-update 文档):

bash
systemctl --user enable --now podman-auto-update.timer
systemctl --user list-timers podman-auto-update.timer

平时想手动看看有哪些容器有新镜像(只检查不更新),可以跑:

bash
podman auto-update --dry-run

真要更新时再去掉 --dry-run:

bash
podman auto-update

说明

我的更新习惯 像 Gitea、Nextcloud 这种存着代码和个人网盘数据的服务,万一跨大版本升级把数据库结构改了,光把镜像退回旧版是救不回来的。所以这类有状态的服务我一般不开自动更新,还是习惯先备份好数据、看一眼升级说明,再手动 podman pull 并 restart 服务。

常用维护命令速查

常见场景操作命令 / 方法
改容器端口/环境变量/挂载改 ~/.config/containers/systemd/*.container → systemctl --user daemon-reload → restart 服务
临时启动 / 停止服务systemctl --user start/stop 名称.service
设置 Quadlet 容器开机自启在 .container 末尾加 [Install] 和 WantedBy=default.target,再 daemon-reload
让一组容器跟着 target 一起停在每个成员 .container 的 [Unit] 里加上 PartOf=app.target
查容器反复启动失败的原因journalctl --user -u 名称.service -b -n 100 --no-pager
清除失败限流状态重新启动systemctl --user reset-failed 名称.service
检查有哪些容器可以更新podman auto-update --dry-run

用 Podman 和 Quadlet 管理家庭服务器容器

https://www.renpengyu.com/articles/podman-quadlet/
作者
Penry
发布于
2026.06.28
许可协议
CC BY-NC-SA 4.0
站内搜索

找到你感兴趣的内容

文章分类
选择文章分类,匹配任意一个所选分类

多选时显示属于任一所选分类的文章。

文章标签
选择文章标签,多个标签需同时包含

多选时只显示同时包含所选标签的文章。

输入关键词开始搜索