Podman Quadlets: Replace Docker Compose with systemd

Photo by Taylor Vick on Unsplash
A Quadlet is an INI-style unit file (.container, .network, .volume, .pod, and more) that describes a container to systemd. The podman-system-generator runs at boot, scans the Quadlet directories, and generates a real .service unit from each file. You manage the container with normal systemctl commands instead of a container-specific CLI.
Rootless user units go in the containers/systemd folder under XDG_CONFIG_HOME, while system-wide rootful units go in /etc/containers/systemd/. The generator only scans a fixed set of search paths, so a unit placed in the wrong directory is silently ignored. After adding a file you run systemctl daemon-reload so the generator picks it up.
For a self-hosted server, yes. Quadlets give you rootless containers, auto-restart, boot ordering, journald logs, and built-in auto-update without the Docker daemon. The trade-off is there is no single up command reading a whole project, so a multi-container app becomes several unit files rather than one docker-compose.yml document.
Set AutoUpdate to registry in the Container section. The podman-auto-update.timer fires daily at midnight, checks the registry for a newer image, pulls it, and restarts the unit. It requires a fully qualified image reference. If the unit fails to restart after the update, Podman rolls back to the previous image automatically.
Rootless systemd services are tied to your user session and stop when you log out unless lingering is enabled. Run loginctl enable-linger for the account so the containers keep running after your SSH session closes and start again after a reboot. Combined with WantedBy set to default.target, this makes them behave like normal system services.

Photo by Taylor Vick on Unsplash
Key Takeaway
Podman Quadlets let you describe containers as native systemd units. You write a plain .container file, and the podman-system-generator turns it into a service systemd manages directly. That gives you rootless containers, auto-restart, boot ordering, journald logs, and built-in registry auto-update without the Docker daemon or a separate Compose tool.
I ran my personal server on Docker Compose for years. It works, but it always felt like a second init system bolted on top of the first: the Docker daemon runs as root, containers restart because a policy string tells the daemon to, and logs live in a JSON file that only the docker CLI knows how to read. When the box reboots, systemd starts the Docker daemon, and the daemon starts my containers. Two supervisors, one job.
Podman Quadlets collapse that stack. Instead of a docker-compose.yml interpreted by a long-running daemon, you write a declarative unit file that systemd itself understands. There is no daemon, the containers can run entirely rootless, and every feature systemd already has for ordinary services applies to them too. This is how I now run everything on my VPS, and I have not missed Compose once.
A Quadlet is a small INI-style file that describes a container the way a systemd unit describes a service. Podman ships a generator called podman-system-generator that runs early in the boot sequence, scans a set of directories, and translates each Quadlet into a real .service unit on the fly. You never write the messy service file by hand, and you never commit generated files to disk — the generator regenerates them on every daemon-reload.
Quadlet handles more than single containers. Alongside the .container file there are .network, .volume, and .pod files for the networks, named volumes, and pods your app needs, plus .kube, .image, .build, and .artifact for more advanced cases. Because a newer Podman release ships an improved generator, your services quietly pick up the fixes the next time daemon-reload runs — the description stays the same while the plumbing underneath gets better.
Here is a working Quadlet for a Ghost blog, dropped into the rootless user directory. It reads almost like the environment section of a Compose file, but the sections map straight onto systemd concepts you may already know: Unit for ordering and dependencies, Container for the Podman-specific options, Service for the restart policy, and Install for what enables the unit at boot.
# ~/.config/containers/systemd/ghost.container
[Unit]
Description=Ghost blog
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/library/ghost:5-alpine
AutoUpdate=registry
PublishPort=2368:2368
Volume=ghost-content.volume:/var/lib/ghost/content
Environment=NODE_ENV=production
Environment=url=https://blog.example.com
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.targetSave that file, then let systemd discover it. A daemon-reload triggers the generator, and from then on the container is an ordinary service you start, stop, enable, and inspect with the same commands you use for anything else systemd runs. The service name is the file name with the extension swapped, so ghost.container becomes ghost.service.
# register the generated service, then start it
systemctl --user daemon-reload
systemctl --user start ghost.service
journalctl --user -u ghost.service -fRootless Quadlets for a user go in the directory at the top of the sample, which maps to XDG_CONFIG_HOME under containers and systemd. System-wide rootful units live under etc, containers, systemd instead. Put the file in the right place first — the generator only scans a fixed set of search paths, so a unit in the wrong directory is silently ignored.
The single biggest reason I switched is rootless. A Quadlet in your user directory runs as your unprivileged user with no daemon and no root, so a container breakout lands inside an ordinary user account rather than on the host as root. Everything else follows from being a first-class systemd unit rather than a daemon-managed process.
Because the container is a real unit, the tools you already trust just work, and there is nothing container-specific to learn:
Rootless services stop when the user logs out unless lingering is enabled. Run loginctl enable-linger for the account, or your containers will die the moment your SSH session closes and never survive a reboot. This is the one gotcha that catches everyone moving self-hosted workloads to rootless Podman — enable linger before you rely on it.
Set AutoUpdate=registry in the Container section, as the sample does, and Podman can update the container for you. The podman-auto-update.timer fires daily at midnight, checks the registry for a newer image behind each auto-update label, pulls it, and restarts the unit. It only works with a fully qualified image reference, because Podman needs an exact name to re-check — a bare tag or an image ID gives it nothing to compare against.
The safety net is rollback. If the unit fails to restart cleanly after an update, Podman reverts to the previous image and restarts the service again, so a bad release does not leave you with a down container at midnight. There is also AutoUpdate=local for images you build on the host yourself, where Podman compares the container against your local image store instead of a remote registry.
Compose and Quadlet solve overlapping problems, but they sit at different layers. Compose is a tool that talks to a daemon; Quadlet is a description that systemd consumes directly. This table is the comparison I wish I had when I started the migration.
| Concern | Docker Compose | Podman Quadlet |
|---|---|---|
| Process model | Long-running Docker daemon, usually root | No daemon; rootless, run directly by systemd |
| Definition format | One docker-compose.yml in YAML | Separate .container, .network, .volume unit files |
| Lifecycle manager | The compose CLI wraps the daemon | systemd manages the generated services natively |
| Restart on boot | restart policy plus the daemon starting first | WantedBy=default.target plus user lingering |
| Logs | docker logs reads a per-container JSON file | journald, queried with journalctl like any service |
| Auto-update | External tool such as Watchtower | Built-in AutoUpdate=registry plus the daily timer |
| Dependency ordering | depends_on inside the compose file | After= and Requires= from systemd |
Quadlet is not a drop-in replacement for every Compose workflow — there is no single up command that reads a whole project, and multi-container apps become several unit files rather than one document. But for a self-hosted server that should boot, restart, log, and update like the rest of the operating system, running containers as systemd units is the cleaner model. Start by porting one service, prove the reboot and auto-update paths, then move the rest.