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.
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.
| Compose | Quadlet |
|---|---|
image | Image= (short names get docker.io/) |
ports | PublishPort= |
volumes (named) | Volume=name.volume:/path plus a .volume file |
environment, env_file | Environment=, EnvironmentFile= |
depends_on | Requires= and After= in [Unit] |
restart: unless-stopped | Restart=always in [Service] |
healthcheck | HealthCmd=, HealthInterval=, HealthRetries= |
command, entrypoint | Exec=, Entrypoint= |
secrets | Secret= (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.