From Compose to Systemd: Elegantly Managing Containers With Podman and Quadlet

Manually written + AI polished

Preface

Podman, as an excellent alternative to Docker, provides a daemon-free container management experience. When we need to manage multiple containers (services) as a single application unit, besides podman-compose, we have a more native and powerful choice: Quadlet.

Quadlet is a Podman tool that allows us to define and manage containers, Pods, and other Podman resources using Systemd unit files. This approach not only enables declarative container deployment but also fully leverages Systemd’s powerful service management capabilities, such as automatic start/stop, dependency management, log integration, etc.

This article will use deploying Immich (a self-hosted photo and video backup solution) as an example to demonstrate step-by-step how to use the podlet tool to easily convert docker-compose.yml files to Quadlet configuration files and ultimately host them through Systemd.

Practice: Converting Immich’s Docker Compose Configuration to Quadlet

Step 1: Obtain and Adjust docker-compose.yml

First, we obtain the latest docker-compose.yml file from Immich’s GitHub Release page.

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

Before conversion, we need to make some adaptive modifications to this file to make it more suitable for Podman and Quadlet environments:

  1. Remove container_name: Quadlet automatically names containers based on unit file names, so we directly use service names (like server, redis) as identifiers.
  2. Replace environment variable references: The original file uses .env files and variable substitution (like ${IMMICH_VERSION}). To simplify the tutorial, we directly replace these variables with specific values.
  3. Adjust inter-service communication: In Podman, containers within the same Pod can communicate directly through 127.0.0.1 (localhost). According to Immich documentation, DB_HOSTNAME and REDIS_HOSTNAME environment variables are used for service discovery. Therefore, we set their values to 127.0.0.1.

The modified docker-compose.yml is as follows:

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

Step 2: Use podlet to Generate Quadlet Files

Now we can use the podlet command to perform the conversion. We pass the modified YAML content through a pipe to podlet.

  • -i (--install): Add an [Install] section to the generated unit files so services will start after being enabled.
  • -a (--absolute-paths): Convert relative paths in the Compose file (like ./database) to absolute paths. This is required because Systemd unit files don’t recognize paths relative to the Compose file location.
  • -f (--file): Optional parameter. If provided, podlet will write output directly to corresponding unit files (like immich-server.container) instead of printing to standard output.

Execute the following command to automatically convert the compose file in the current path:

podlet -i -a compose --pod

podlet will generate a series of .container and .pod files, which are unit files that Systemd can understand. The output content is as follows:

# 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

Step 3: Deploy and Manage Services

  1. Save Unit Files: Save the above output content by filename (like # immich-server.container) to the ~/.config/containers/systemd/ directory. This is the standard location where Podman stores user-level Systemd unit files.

  2. Reload Systemd: Notify Systemd to reload configuration files.

    systemctl --user daemon-reload
  3. Create Related Directories: Mounted directories need to be created manually beforehand, otherwise container startup will fail.

  4. Start Services: Start the entire Pod. Systemd will automatically start all containers within the Pod based on dependency relationships.

    systemctl --user start immich-pod
  5. Check Status: You can check the running status of Pods and containers at any time.

    systemctl --user status immich-*

At this point, the Immich application has successfully run through Quadlet and Systemd. You can also use systemctl --user start/stop/restart to control individual containers separately.

However, you’ll soon discover some small issues, which we’ll solve next.

Step 4: Solve Remaining Issues

1. Database Container Mount Directory Ownership Issues

Problem Manifestation:
After starting the database container, you find that files in the database directory mounted to the host (like database:/var/lib/postgresql/data) are owned not by the current user, but by a high UID (like 100999), making it impossible to directly access or modify these files on the host.

Solution:
Add UserNS=keep-id to the [Pod] section of the immich.pod file to keep container user UID/GID consistent with the host. This way, files created within the container will directly belong to the current host user, avoiding permission and ownership issues.

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

...

2. Immich Container Continuously Restarting After Running for a While

Problem Manifestation:

This issue appears after solving the previous problem. After starting the immich container, it continuously restarts after running for some time, with the error:

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.

Checking valkey logs reveals that valkey’s RDB file cannot persist to disk.

Failed opening the temp RDB file temp-1.rdb (in server root dir /data) for saving: Permission denied

Solution:

Add User and Group parameters to the [Container] section of the immich-redis.container file and set them to root.

# in immich-redis.container
[Container]
...
User=root
Group=root
...

Podman Other Common Q&A

1. Permission Issues When Container Files Are Mounted Locally

When running containers in rootless mode, container user UID/GID doesn’t match host users, potentially causing permission errors on mounted volumes.

Solution: Add UserNS=keep-id to the [Pod] section of .pod files. This preserves the host user’s UID/GID, ensuring consistent file permissions.

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

2. When Using Pasta Network, Container Cannot Access Host Services via Public IP

This is a known behavior in newer Podman versions (after defaulting to Pasta network backend). The container’s network namespace shares the host’s public IP, causing access to the public IP to actually access the container itself.

Solution: Configure an independent internal network for Pasta. Write the following content to ~/.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"]

References:

3. Systemd User Services Terminated After User Logout

By default, Systemd user sessions end when users log out, terminating all services started by that session.

Solution: Enable “linger” for your user to keep the session active after logout.

sudo loginctl enable-linger <your-username>

4. How to Auto-Update Container Images?

Quadlet can configure Podman to automatically pull the latest container images.

Solution:

  1. Add the AutoUpdate=registry label to the [Container] section in .container files for containers that need auto-updates.
  2. Enable and start Podman’s auto-update timer service.
systemctl --user enable --now podman-auto-update.timer

This way, Podman will periodically check and pull new versions of images.

5. Podlet Conversion Errors

When encountering podlet conversion errors, it’s usually because the docker-compose.yml file contains unsupported or incompatible configuration items. Simply adjust the compose file content according to podlet’s error prompts (such as removing unsupported fields, correcting formats, etc.), then re-execute the conversion command. Generally, podlet’s error messages clearly indicate the problem location, and following the prompts for modifications will allow smooth generation of Quadlet files.

0%