# Quadlet 实战：Immich


> 官方 docker-compose 文件已经改动，建议仅参考流程

如果你还不熟悉 Quadlet 的基本概念，建议先阅读 [Podman Quadlet 入门](/posts/podman/)。

本文以部署 [Immich](https://immich.app/) 这个自托管照片和视频备份方案为例，演示如何使用 `podlet` 将 `docker-compose.yml` 转换为 Quadlet 配置，并最终交给 Systemd 托管。

## 1. 获取并调整 `docker-compose.yml`

先从 Immich 的 GitHub Release 页面获取最新的 `docker-compose.yml`：

```bash
wget https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
```

转换前需要先做几处适配：

1. **移除 `container_name`**：Quadlet 会根据单元文件名自动命名容器。
2. **替换环境变量引用**：将 `${IMMICH_VERSION}` 等变量替换为具体值。
3. **调整服务间通信**：同一 Pod 内的容器通过 `127.0.0.1` 通信，因此 `DB_HOSTNAME` 和 `REDIS_HOSTNAME` 需要设为 `127.0.0.1`。

修改后的 `docker-compose.yml` 示例：

```yaml
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:9
    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

volumes:
  model-cache:
```

## 2. 使用 `podlet` 生成 Quadlet 文件

[`podlet`](https://github.com/containers/podlet) 是一个用 Rust 编写的 CLI 工具，由维护 Podman 的 `containers` 组织维护。它可以从 `docker-compose.yml`、`docker run` 命令或现有的 Podman 对象生成 Quadlet 文件，是 Compose 迁移里非常省事的工具。

用 `podlet` 自动转换时，下面几个参数比较常用：

- `-i`：在生成的单元文件中增加 `[Install]` 部分。
- `-a`：将相对路径转换为绝对路径，因为 Systemd 单元文件不识别相对路径。
- `-f`：直接写入对应的单元文件，而不是输出到标准输出。

```shell
podlet -i -a -f compose --pod
```

为便于阅读，下面把生成的多个文件内容合并展示：

```ini
# immich-server.container
[Unit]
Requires=immich-redis.service immich-database.service
After=immich-redis.service immich-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=/path/to/server/upload:/usr/src/app/upload
Volume=/etc/localtime:/etc/localtime:ro

[Service]
Restart=always

[Install]
WantedBy=default.target

---

# immich-machine-learning.container
[Container]
Image=ghcr.io/immich-app/immich-machine-learning:release
Pod=immich.pod
Volume=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:9
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=/path/to/database:/var/lib/postgresql/data

[Service]
Restart=always

[Install]
WantedBy=default.target

---

# immich.pod
[Pod]
PublishPort=2283:2283

[Install]
WantedBy=default.target
```

目前 `podlet` 还有一个小 bug：不会自动为命名卷生成对应的 Quadlet 配置文件，这里需要手动补一个：

```ini
# immich-model-cache.volume
[Volume]
```

然后再把 `immich-machine-learning.container` 文件中的 `model-cache:/cache` 改成 `immich-model-cache.volume:/cache`。

## 3. 部署和管理服务

1. **保存文件**：将上述文件放到 `~/.config/containers/systemd/`。
2. **重载 Systemd**：

   ```shell
   systemctl --user daemon-reload
   ```

3. **创建目录**：绑定挂载用到的目录需要提前手动创建，否则容器启动会失败。
4. **启动 Pod**：Systemd 会根据依赖自动启动所有容器。

   ```shell
   systemctl --user start immich-pod
   ```

5. **检查状态**：

   ```shell
   systemctl --user status immich-*
   ```

至此，Immich 就已经通过 Quadlet + Systemd 运行起来了。之后你可以用 `systemctl --user start/stop/restart` 分别控制单个容器。

不过在实际部署时，通常还会遇到一些细节问题，下面继续处理。

## 4. 解决常见问题

### I. 数据库容器挂载目录所有权问题

**现象**：启动 `database` 后，挂载目录下的文件所有者会显示为一个异常的高位 UID（例如 `100998`），宿主机用户无法直接访问这些文件。

**原因**：Rootless Podman 默认使用用户命名空间映射，容器内的 UID 会映射到宿主机的 subuid 范围。Postgres 容器默认以 UID `999` 运行，所以在宿主机上看到的就会变成一个高位 UID，文件所有权也不再对应当前用户。

**解决**：在 `immich.pod` 的 `[Pod]` 部分添加 `UserNS=keep-id:uid=999`。这样可以让宿主机当前用户在容器内以 UID `999` 的身份运行，从而让数据库写出的文件在宿主机上仍然归当前用户所有。

```ini
# immich.pod
[Pod]
PublishPort=2283:2283
UserNS=keep-id:uid=999
```

### II. `podlet` 转换失败

`podlet` 转换失败，通常是因为 `docker-compose.yml` 中包含了它暂不支持的配置项。根据 `podlet` 的报错提示删除不支持的字段、调整格式后，再重新执行转换即可。


---

> 作者: Nite  
> URL: https://www.nite07.com/zh-cn/posts/podman-quadlet-immich/  

