从 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 的环境:

  1. 移除 container_name:Quadlet 会根据 unit 文件名自动为容器命名,我们直接使用 service 的名称(如 server, redis)作为标识。
  2. 替换环境变量引用:原始文件使用了 .env 文件和变量替换(如 ${IMMICH_VERSION})。为了简化教程,我们直接将这些变量替换为具体的值。
  3. 调整服务间通信:在 Podman 中,同一个 Pod 内的容器可以通过 127.0.0.1 (localhost) 直接通信。根据 Immich 文档,DB_HOSTNAMEREDIS_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

第三步:部署和管理服务

  1. 保存 Unit 文件: 将上述输出内容根据文件名(如 # immich-server.container)分别保存到 ~/.config/containers/systemd/ 目录下。这是 Podman 存放用户级 Systemd unit 文件的标准位置。

  2. 重载 Systemd: 通知 Systemd 重新加载配置文件。

    systemctl --user daemon-reload
  3. 创建相关目录:挂载的目录需要提前手动创建,不然容器会启动失败。

  4. 启动服务: 启动整个 Pod。Systemd 会根据依赖关系,自动启动该 Pod 内的所有容器。

    systemctl --user start immich-pod
  5. 检查状态: 你可以随时检查 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"]

参考:

3. 用户登出后,Systemd 用户服务被终止

默认情况下,Systemd 用户会话(user session)会在用户登出时结束,从而终止所有该会话启动的服务。

解决方案:为你的用户启用 “linger”,使其会话在登出后依然保持活动。

sudo loginctl enable-linger <你的用户名>

4. 如何让容器镜像自动更新?

Quadlet 可以配置 Podman 自动拉取最新的容器镜像。

解决方案

  1. 在需要自动更新的容器的 .container 文件中,向 [Container] 部分添加 AutoUpdate=registry 标签。
  2. 启用并启动 Podman 的自动更新定时器服务。
systemctl --user enable --now podman-auto-update.timer

这样,Podman 就会定期检查并拉取新版本的镜像。

5. Podlet 转换错误

遇到 podlet 转换错误时,通常是因为 docker-compose.yml 文件中存在不被支持或不兼容的配置项。只需根据 podlet 的错误提示,逐项调整 compose 文件内容(如移除不支持的字段、修正格式等),然后重新执行转换命令即可。一般来说,podlet 的报错信息会明确指出问题所在,按照提示修改后即可顺利生成 Quadlet 文件。

0%