Podman 6.0: running a rootless container as a systemd service with Quadlet
How to run a rootless container as a clean, boot-capable systemd service with Podman 6.0 and Quadlet – with a working Caddy example and the common pitfalls.
If you come from Docker Compose, you know the pattern: a container as a service that starts at boot and restarts after a crash. Under Podman this role is not played by Compose but by systemd – and for a while now you describe such services declaratively with Quadlet. You write a short .container file, and systemd generates a full-fledged service unit from it. In Podman 6.0 (June 2026) Quadlet is the default path and even more tightly integrated. This article stands up a single rootless service: drop a .container file, daemon-reload, start it, check it, make it boot-capable.
What Quadlet does – and when it fits
Quadlet is a systemd generator that produces a .service unit at runtime from a .container file. You no longer maintain hand-written units with long ExecStart=podman run ... lines, but a compact, ini-style description in the familiar Compose spirit (image, ports, volumes, environment variables). This fits exactly when you want to run one or a few services on a single host rootless and boot-capable – for example on a VPS or self-hosting server. For orchestration across multiple nodes, Kubernetes remains the tool of choice.
Rootless means the container runs in the user namespace of your normal login, not as root. Accordingly, we work throughout with systemctl --user, not with the system systemd.
One note about Podman 6.0 up front: cgroup v1 is no longer supported, and neither are CNI, slirp4netns, or iptables. Rootless networking runs via pasta, firewalling via nftables. On a current Linux (cgroup v2, netavark/pasta) this is the normal case – on very old hosts you should verify this before upgrading.
The minimal service
We run a simple web server (Caddy) as a rootless service. Place the Quadlet file in the rootless search path:
mkdir -p ~/.config/containers/systemd
File ~/.config/containers/systemd/web.container:
[Unit]
Description=Caddy web server (rootless, Quadlet)
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/library/caddy:2
PublishPort=8080:80
Volume=web-content:/usr/share/caddy:Z
Environment=TZ=UTC
[Service]
Restart=on-failure
[Install]
WantedBy=default.target
Then have the generator read it and start the service:
systemctl --user daemon-reload
systemctl --user start web.service
systemctl --user status web.service
status should show active (running). A quick test:
curl -I http://localhost:8080
Caddy answers on port 8080; Podman creates the named volume web-content automatically on first start.
The building blocks of the .container file
[Container]is the core.Image=specifies the image (fully qualified, including the registry, to avoid ambiguity).PublishPort=8080:80maps host port 8080 to container port 80.Volume=web-content:/usr/share/caddy:Zmounts a named volume; the:Zsets the SELinux label appropriately if your system uses SELinux.Environment=sets environment variables.[Service]holds classic systemd service options.Restart=on-failurerestarts the container after a crash – the counterpart to the Compose restart policy.[Install]withWantedBy=default.targetanchors the service so that it starts together with your user session.
The flow at a glance:
web.container (~/.config/containers/systemd/)
|
| systemctl --user daemon-reload
v
+---------------------+
| Quadlet generator | builds unit at runtime
+----------+----------+
|
v
web.service (transient)
|
| systemctl --user start web.service
v
+---------------------+
| rootless container | caddy:2 -> host :8080
+---------------------+
Autostart without an active login
WantedBy=default.target alone is not enough when nobody is logged in: by default the user manager only runs while a session is open. So that your service also starts after a reboot without a login, enable linger for your user:
loginctl enable-linger
From now on, systemd starts your user manager at boot and keeps it alive after logout – the container runs permanently.
Three common pitfalls
1. The service name is the file name, not the container name. web.container becomes web.service. So you control the service with systemctl --user start web.service – not via a name set inside the container. Mixing this up leads to "Unit not found".
2. No systemctl enable for Quadlet services. From systemd's point of view the generated units are transient and cannot be anchored for boot with systemctl enable. Autostart is controlled exclusively via [Install] WantedBy= in the .container file plus daemon-reload. An enable attempt fails.
3. Ports below 1024 don't work rootless out of the box. As a normal user you may not bind privileged ports (< 1024) by default. That is why it's PublishPort=8080:80 instead of 80:80. If you want to expose the service externally on port 80/443, put a reverse proxy in front of it or adjust net.ipv4.ip_unprivileged_port_start – for getting started, stick to a high host port.
Where to go from here
You now have a clean, rootless pattern that transfers to every further service: .container file, daemon-reload, --user start, enable-linger. Obvious next steps are dedicated networks (.network) and volumes (.volume) as further Quadlet files, as well as grouping several containers into a pod (.pod). The details are in the official documentation: podman-systemd.unit (Quadlet) and the Podman documentation.
Note: The articles on this blog are produced with the help of AI and are editorially reviewed before publication. Editorial responsibility lies with Emre Yurtbay (see the Impressum).