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:
- Remove
container_name
: Quadlet automatically names containers based on unit file names, so we directly use service names (likeserver
,redis
) as identifiers. - 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. - 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
andREDIS_HOSTNAME
environment variables are used for service discovery. Therefore, we set their values to127.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 (likeimmich-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
-
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. -
Reload Systemd: Notify Systemd to reload configuration files.
systemctl --user daemon-reload
-
Create Related Directories: Mounted directories need to be created manually beforehand, otherwise container startup will fail.
-
Start Services: Start the entire Pod. Systemd will automatically start all containers within the Pod based on dependency relationships.
systemctl --user start immich-pod
-
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:
- Podman v5.0 Breaking Changes in Detail
- Podman 5.3 Changes for Improved Networking Experience with Pasta
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:
- Add the
AutoUpdate=registry
label to the[Container]
section in.container
files for containers that need auto-updates. - 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.