Quadlet in Practice: Immich

Contents

The official docker-compose file has been modified; it is recommended to use it only as a reference for the process.

If you are not yet familiar with the basics of Quadlet, it is worth reading Podman Quadlet Basics first.

This article uses Immich, a self-hosted photo and video backup solution, to walk through how to use podlet to convert docker-compose.yml into Quadlet files and then hand the whole deployment over to Systemd.

1. Fetch and Adjust docker-compose.yml

First, fetch the latest docker-compose.yml from Immich’s GitHub release page:

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

Before converting it, make a few adjustments:

  1. Remove container_name: Quadlet names containers automatically based on the unit filename.
  2. Replace environment variable references: Replace ${IMMICH_VERSION} and similar variables with concrete values.
  3. Adjust inter-service communication: Containers inside the same Pod communicate over 127.0.0.1, so DB_HOSTNAME and REDIS_HOSTNAME should both be set to 127.0.0.1.

Example of the adjusted 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: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. Generate Quadlet Files with podlet

podlet is a CLI tool written in Rust and maintained by the containers project behind Podman. It can generate Quadlet files from docker-compose.yml, docker run commands, or existing Podman objects, making it one of the most convenient tools for Compose migrations.

These options are especially useful when converting with podlet:

  • -i: add an [Install] section to the generated unit files.
  • -a: convert relative paths into absolute paths, because Systemd unit files do not understand relative paths.
  • -f: write directly to the corresponding unit files instead of printing to standard output.
podlet -i -a -f compose --pod

For readability, the generated files are combined below:

# 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

At the moment, podlet has a small bug: it does not automatically generate the corresponding Quadlet file for named volumes, so you need to add one manually:

# immich-model-cache.volume
[Volume]

Then change model-cache:/cache to immich-model-cache.volume:/cache in immich-machine-learning.container.

3. Deploy and Manage the Services

  1. Save the files: place the files above under ~/.config/containers/systemd/.

  2. Reload Systemd:

    systemctl --user daemon-reload
  3. Create directories: create the bind-mount directories in advance, otherwise the containers will fail to start.

  4. Start the Pod: Systemd will automatically start all containers based on the dependency graph.

    systemctl --user start immich-pod
  5. Check status:

    systemctl --user status immich-*

At this point, Immich is already running under Quadlet + Systemd. After that, you can use systemctl --user start/stop/restart to control individual containers.

In practice, though, you will usually run into a few details worth handling next.

4. Solve Common Issues

I. Ownership of the Database Mount Directory

Symptom: After starting database, files under the mounted directory show an unusually high UID such as 100998, and the host user cannot conveniently access them.

Cause: Rootless Podman uses user namespace mapping by default, so UIDs inside the container are mapped into the host’s subuid range. The Postgres container runs as UID 999, which therefore appears as a high UID on the host, and file ownership no longer matches the current host user.

Fix: Add UserNS=keep-id:uid=999 to the [Pod] section of immich.pod. This makes the current host user appear as UID 999 inside the container, so files written by PostgreSQL are still owned by the current user on the host side.

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

II. podlet Conversion Failures

When podlet fails to convert, the usual cause is that the docker-compose.yml file contains fields it does not support yet. Remove the unsupported fields or adjust the format according to the error message, then run the conversion again.

Edit this page

Contents