docker-compose to quadlet converter

Turn a compose file into .container, .volume, .network and .pod files for systemd.

Last updated

In short: Each compose service becomes a .container file, each named volume a .volume file, and the shared network a .network file, so systemd starts, orders and restarts the containers. depends_on becomes Requires= and After=, and restart: becomes Restart=. Keys with no quadlet equivalent are listed as warnings, never dropped silently. Save the files in ~/.config/containers/systemd/ and run systemctl --user daemon-reload.

Your input never leaves your browser. Everything is calculated on your device; nothing you type is sent to a server.

Options

Generated files

# Converted from compose service "app".

[Unit]
Description=app container
Requires=db.service
After=db.service

[Container]
Image=docker.io/library/wordpress:6
ContainerName=app
Network=app.network
PublishPort=8080:80
Volume=wordpress.volume:/var/www/html
Environment=WORDPRESS_DB_HOST=db
Environment=WORDPRESS_DB_USER=wordpress
Environment=WORDPRESS_DB_PASSWORD=change-me
Environment=WORDPRESS_DB_NAME=wordpress

[Service]
Restart=always

[Install]
WantedBy=default.target

Warnings and notes

  • Note interpolation: Used the default value for ${DB_PASSWORD} (not set in the .env box).
  • Note volumes: Podman names quadlet volumes systemd-<name>, so volumes created earlier by compose (<project>_<name>) are not reused. Add VolumeName=<existing name> to a .volume file to keep existing data.

Save the files in ~/.config/containers/systemd/, run systemctl --user daemon-reload, then systemctl --user start <name>.service. Run loginctl enable-linger $USER once so rootless containers start at boot without a login. The files target podman 5.0 or newer unless you tick the 4.9 option.

How does the docker-compose to quadlet converter work?

A quadlet is a small file that describes one container, volume, network or pod. At boot, and whenever you run systemctl --user daemon-reload, podman's quadlet generator turns each file into a systemd service. From then on systemd does the job that docker compose up did: it starts containers in order, restarts them when they crash, and writes their logs to the journal (journalctl --user -u web).

The conversion is mostly one key to one line. The table shows the common cases; everything else is listed under the output, either as a note or as a warning when you need to act.

How compose keys map to quadlet keys
ComposeQuadlet
imageImage= (short names get docker.io/)
portsPublishPort=
volumes (named)Volume=name.volume:/path plus a .volume file
environment, env_fileEnvironment=, EnvironmentFile=
depends_onRequires= and After= in [Unit]
restart: unless-stoppedRestart=always in [Service]
healthcheckHealthCmd=, HealthInterval=, HealthRetries=
command, entrypointExec=, Entrypoint=
secretsSecret= (podman secrets)

Compose puts every stack on a shared network where services find each other by name. The converter does the same with one .network file and sets ContainerName= to the service name, so db:5432 keeps working. A .pod is only created when containers must share one network namespace, because inside a pod they talk over localhost instead.

Two details catch people out. Values are escaped for systemd: a literal % becomes %% and $ becomes $$, which is why a bcrypt hash looks doubled. And relative paths such as ./data resolve against the folder holding the .container file, not your compose folder, so use absolute paths. The key reference is the podman-systemd.unit(5) man page.

Worked examples

Publish a port

Input ports: ["8080:80"] gives PublishPort=8080:80

Ports keep the same host:container syntax. Host ports below 1024 get a warning, because rootless podman cannot bind them by default.

Wait for a healthy database

Input depends_on: { db: { condition: service_healthy } } gives Requires=db.service + After=db.service, Notify=healthy in db.container

With Notify=healthy, systemd only marks db.service as started once its healthcheck passes, so After= really waits for a working database.

Route a container through a VPN container

Input network_mode: "service:gluetun" gives Pod=gluetun.pod, ports moved to gluetun.pod

Containers that share one network namespace go into one pod. The pod publishes the ports, and the containers reach each other on localhost.

Frequently asked questions

How do I convert docker-compose to podman quadlet?

Paste the compose file above and save each generated file in ~/.config/containers/systemd/. Then run systemctl --user daemon-reload and systemctl --user start <service>.service. The quadlet generator described in the podman-systemd.unit(5) man page turns each file into a normal systemd service.

Can rootless podman use ports below 1024?

Not by default: unprivileged users may only bind ports from 1024 up. Either publish a high port (PublishPort=8080:80) behind a reverse proxy, or allow low ports system-wide with net.ipv4.ip_unprivileged_port_start=80 in a file under /etc/sysctl.d/.

How does depends_on work in quadlet?

Each dependency becomes Requires=<name>.service and After=<name>.service in the [Unit] section. condition: service_healthy also adds Notify=healthy to the dependency (podman 5.0 or newer), so the dependent container starts only when the healthcheck passes.

Where do passwords from .env and compose secrets go?

Paste your .env into the .env box to fill in ${VAR} values, but keep real passwords out of unit files. Create a podman secret (podman secret create db_password ./file) and reference it with Secret=db_password; compose secrets are converted this way automatically.

When do I need a .pod file instead of a .network file?

Only when containers must share one network namespace, as with network_mode: service:<name>. For normal stacks, one .network file is enough: containers on it resolve each other by name, just like compose's default network.

Which podman version do the files need?

Podman 5.0 or newer by default. Tick the podman 4.9 option for older systems: settings that 4.9 does not know become PodmanArgs= flags, and pods are replaced by Network=container:<name>. The test suite checks both versions with the quadlet generator and systemd-analyze verify.