从 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 --pod
podlet
会生成一系列 .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-id
2. 使用 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 文件。