# Quadlet in Practice: Immich


> 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](/posts/podman/) first.

This article uses [Immich](https://immich.app/), 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:

```bash
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`:

```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. Generate Quadlet Files with `podlet`

[`podlet`](https://github.com/containers/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.

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

For readability, the generated files are combined below:

```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
```

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:

```ini
# 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**:

   ```shell
   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.

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

5. **Check status**:

   ```shell
   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.

```ini
# 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.


---

> Author: Nite  
> URL: https://www.nite07.com/en/posts/podman-quadlet-immich/  

