从 Compose 到 Systemd:使用 Podman 和 Quadlet 优雅地管理容器
人工编写 + AI 润色
前言
Podman 作为 Docker 的一个优秀替代品,提供了无守护进程的容器管理体验。当我们需要将多个容器(服务)作为一个应用单元进行管理时,除了 podman-compose,我们还有一个更原生、更强大的选择:Quadlet。
Quadlet 是 Podman 的一个工具,它允许我们使用 Systemd unit 文件来定义和管理容器、Pod 以及其他 Podman 资源。这种方式不仅可以实现容器的声明式部署,还能充分利用 Systemd 强大的服务管理能力,如自动启停、依赖管理、日志集成等。
本文将以部署 Immich(一个自托管的照片和视频备份解决方案)为例,一步步展示如何使用 podlet 工具,轻松地将 docker-compose.yml 文件转换为 Quadlet 配置文件,并最终通过 Systemd 进行托管。
实战:将 Immich 的 Docker Compose 配置转换为 Quadlet
第一步:获取并调整 docker-compose.yml
首先,我们从 Immich 的 GitHub Release 页面获取最新的 docker-compose.yml 文件。
wget https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml在进行转换之前,我们需要对这个文件进行一些适配性修改,使其更适合 Podman 和 Quadlet 的环境:
- 移除
container_name:Quadlet 会根据 unit 文件名自动为容器命名,我们直接使用 service 的名称(如server,redis)作为标识。 - 替换环境变量引用:原始文件使用了
.env文件和变量替换(如${IMMICH_VERSION})。为了简化教程,我们直接将这些变量替换为具体的值。 - 调整服务间通信:在 Podman 中,同一个 Pod 内的容器可以通过
127.0.0.1(localhost) 直接通信。根据 Immich 文档,DB_HOSTNAME和REDIS_HOSTNAME环境变量用于服务发现。因此,我们将它们的值设置为127.0.0.1。
修改后的 docker-compose.yml 如下:
name: immich
services:
server:
image: ghcr.io/immich-app/immich-server:release
volumes:
- ./server/upload:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
ports:
- "2283:2283"
depends_on:
- redis
- database
environment:
DB_HOSTNAME: 127.0.0.1
REDIS_HOSTNAME: 127.0.0.1
restart: always
healthcheck:
disable: false
machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:release
volumes:
- ./model-cache:/cache
restart: always
healthcheck:
disable: false
redis:
image: docker.io/valkey/valkey:8-bookworm
healthcheck:
test: redis-cli ping || exit 1
restart: always
database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: immich
POSTGRES_INITDB_ARGS: "--data-checksums"
volumes:
- ./database:/var/lib/postgresql/data
restart: always第二步:使用 podlet 生成 Quadlet 文件
现在,我们可以使用 podlet 命令来执行转换。我们将修改后的 YAML 内容通过管道传递给 podlet。
-i(--install): 在生成的 unit 文件中增加[Install]部分,这样服务就会在开启后启动。-a(--absolute-paths): 将 Compose 文件中的相对路径(如./database)转换为绝对路径。这是必需的,因为 Systemd unit 文件不识别相对于 Compose 文件位置的路径。-f(--file): 可选参数,如果提供,podlet会将输出直接写入对应的 unit 文件(如immich-server.container),而不是打印到标准输出。
执行以下命令,会自动转换当前路径中的 compose 文件:
podlet -i -a compose --podpodlet 会生成一系列 .container 和 .pod 文件,它们是 Systemd 能理解的 unit 文件。输出内容如下:
# immich-immich-server.container
[Unit]
Requires=redis.service database.service
After=redis.service database.service
[Container]
Environment=DB_HOSTNAME=127.0.0.1 REDIS_HOSTNAME=127.0.0.1
Image=ghcr.io/immich-app/immich-server:release
Pod=immich.pod
Volume=/home/nite/tmp/server/upload:/usr/src/app/upload
Volume=/etc/localtime:/etc/localtime:ro
[Service]
Restart=always
[Install]
WantedBy=default.target
---
# immich-immich-machine-learning.container
[Container]
Image=ghcr.io/immich-app/immich-machine-learning:release
Pod=immich.pod
Volume=/home/nite/tmp/model-cache:/cache
[Service]
Restart=always
[Install]
WantedBy=default.target
---
# immich-redis.container
[Container]
HealthCmd=redis-cli ping || exit 1
Image=docker.io/valkey/valkey:8-bookworm
Pod=immich.pod
[Service]
Restart=always
[Install]
WantedBy=default.target
---
# immich-database.container
[Container]
Environment=POSTGRES_PASSWORD=postgres POSTGRES_USER=postgres POSTGRES_DB=immich POSTGRES_INITDB_ARGS=--data-checksums
Image=ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
Pod=immich.pod
Volume=/home/nite/tmp/database:/var/lib/postgresql/data
[Service]
Restart=always
[Install]
WantedBy=default.target
---
# immich.pod
[Pod]
PublishPort=2283:2283
[Install]
WantedBy=default.target第三步:部署和管理服务
-
保存 Unit 文件: 将上述输出内容根据文件名(如
# immich-server.container)分别保存到~/.config/containers/systemd/目录下。这是 Podman 存放用户级 Systemd unit 文件的标准位置。 -
重载 Systemd: 通知 Systemd 重新加载配置文件。
systemctl --user daemon-reload -
创建相关目录:挂载的目录需要提前手动创建,不然容器会启动失败。
-
启动服务: 启动整个 Pod。Systemd 会根据依赖关系,自动启动该 Pod 内的所有容器。
systemctl --user start immich-pod -
检查状态: 你可以随时检查 Pod 和容器的运行状态。
systemctl --user status immich-*
至此,Immich 应用已经成功地通过 Quadlet 和 Systemd 运行起来了。你也可以使用 systemctl --user start/stop/restart 来分别控制单个容器。
但是很快会发现一些小问题,接下来我们一一解决。
第四步:解决剩下的几个小问题
1. 数据库容器挂载目录出现所有权问题
问题表现:
启动 database 容器后,发现挂载到宿主机的数据库目录(如 database:/var/lib/postgresql/data)下的文件所有者不是当前用户,而是一个高位 UID(如 100999),导致在宿主机上无法直接访问或修改这些文件。
解决方法:
在 immich.pod 文件的 [Pod] 部分添加 UserNS=keep-id,让容器内的用户 UID/GID 保持与宿主机一致。这样,容器内创建的文件会直接归属于当前宿主机用户,避免权限和所有权问题。
# immich.pod
[Pod]
PublishPort=2283:2283
UserNS=keep-id
...2. 运行一段时间后 immich 容器不停重启
问题表现:
这个问题会在上一个问题解决后出现。启动 immich 容器后,运行一段时间后,容器会不停重启。并报错:
ERROR [Microservices:MetadataService] Unable to initialize reverse geocoding: ReplyError: MISCONF Valkey is configured to save RDB snapshots, but it's currently unable to persist to disk.查看 valkey 的日志,发现是 valkey 的 RDB 文件无法持久化到磁盘。
Failed opening the temp RDB file temp-1.rdb (in server root dir /data) for saving: Permission denied解决方法:
在 immich-redis.container 文件的 [Container] 部分添加 User Group 参数,并设置为 root。
# in immich-redis.container
[Container]
...
User=root
Group=root
...Podman 其他常见问题解答
1. 容器中的文件挂载到本地后出现权限问题
当以无根模式(rootless)运行容器时,容器内的用户 UID/GID 与宿主机上的用户不匹配,可能导致挂载卷的权限错误。
解决方案:在 .pod 文件的 [Pod] 部分增加 UserNS=keep-id。这会保持宿主机用户的 UID/GID,确保文件权限一致。
# in immich.pod
[Pod]
PublishPort=2283:2283
UserNS=keep-id2. 使用 Pasta 网络时,容器无法通过公网 IP 访问宿主机服务
这是较新版本 Podman(默认使用 Pasta 网络后端)中的一个已知行为。容器的网络命名空间共享了宿主机的公网 IP,导致访问公网 IP 时实际上是访问容器自身。
解决方案:为 Pasta 配置一个独立的内部网络。将下方内容写入 ~/.config/containers/containers.conf:
[network]
pasta_options = ["--address", "10.0.2.0", "--netmask", "24", "--gateway", "10.0.2.2", "--dns-forward", "10.0.2.3"]参考:
- Podman v5.0 Breaking Changes in Detail
- Podman 5.3 Changes for Improved Networking Experience with Pasta
3. 用户登出后,Systemd 用户服务被终止
默认情况下,Systemd 用户会话(user session)会在用户登出时结束,从而终止所有该会话启动的服务。
解决方案:为你的用户启用 “linger”,使其会话在登出后依然保持活动。
sudo loginctl enable-linger <你的用户名>4. 如何让容器镜像自动更新?
Quadlet 可以配置 Podman 自动拉取最新的容器镜像。
解决方案:
- 在需要自动更新的容器的
.container文件中,向[Container]部分添加AutoUpdate=registry标签。 - 启用并启动 Podman 的自动更新定时器服务。
systemctl --user enable --now podman-auto-update.timer这样,Podman 就会定期检查并拉取新版本的镜像。
5. Podlet 转换错误
遇到 podlet 转换错误时,通常是因为 docker-compose.yml 文件中存在不被支持或不兼容的配置项。只需根据 podlet 的错误提示,逐项调整 compose 文件内容(如移除不支持的字段、修正格式等),然后重新执行转换命令即可。一般来说,podlet 的报错信息会明确指出问题所在,按照提示修改后即可顺利生成 Quadlet 文件。