Podman Containers as systemd Units
A nine-line `.container` file becomes a real systemd service, and `systemctl --user enable` then fails on it because the unit is generated, not installed. Stop the container by hand and systemd puts it back within three seconds.
systemd and Quadlet Guide 21 of 47 Intermediate
- OSUbuntu 26.04 LTS (resolute)
- Podman5.7.0
- Runtimecrun 1.21
- Networknetavark 1.16.1
- TimeAbout 16 min
- Reviewed22 August 2026
Written against the versions above. Podman follows the distribution here rather than a vendor repository, so the version you get is the one Ubuntu shipped. The commands are stable across 5.x.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| PODMAN01 | 192.168.0.24 | Ubuntu 26.04 LTS | Primary Container Host | 2 Core | 4 GB | 50 GB |
Before you start
- Podman installed - see guide 1.
- guide 2 is the reason this track exists: with no daemon to restart your containers, systemd is what does it.
-
The generator, and where files go
Quadlet is not a command you run. It is a systemd generator at
/usr/libexec/podman/quadlet, which systemd executes on everydaemon-reloadto turn your files into units.Rootless files live in
~/.config/containers/systemd/. Rootful ones go in/etc/containers/systemd/. The directory is what decides which Podman runs the container, which follows from guide 14 - the two stores are separate machines.bash Example session ls -l /usr/libexec/podman/quadlet-rwxr-xr-x 1 root root 2384344 Feb 23 15:35 /usr/libexec/podman/quadletmkdir -p ~/.config/containers/systemd && ls ~/.config/containers/systemdExpected resultThe generator binary, and an empty unit directory.
Success conditionYou know which directory systemd will read and which generator reads it.
-
Nine lines that describe a service
The file is
web.container- the extension matters, and it is what tells the generator this describes a container rather than a volume or a network.Three sections do the work:
[Container]is Quadlet's own.Image=,PublishPort=andContainerName=are first-class keys, not flags in a command line.[Service]is ordinary systemd.Restart=alwaysis systemd's restart policy, not Podman's - and this is the point of the whole track.[Install]withWantedBy=default.targetis what makes it start automatically. Remember this line; step 3 shows why it matters.
Nothing here is a
podman runinvocation. You are describing the desired state and letting the generator write the command.bash Example session cat > ~/.config/containers/systemd/web.container <<'UNIT'[Unit]Description=nginx, supervised by systemd [Container]Image=docker.io/library/nginx:alpinePublishPort=8090:80ContainerName=quadlet-web [Service]Restart=always [Install]WantedBy=default.targetUNITcat ~/.config/containers/systemd/web.container[Unit]Description=nginx, supervised by systemd [Container]Image=docker.io/library/nginx:alpinePublishPort=8090:80ContainerName=quadlet-web [Service]Restart=always [Install]WantedBy=default.targetExpected resultA nine-line unit file with
[Container],[Service]and[Install].Success conditionThe file exists and names an image and a port.
-
daemon-reload writes the unit
systemctl --user daemon-reloadruns the generator.list-unit-filesthen showsweb.servicewith stategenerated- notenabled, notdisabled, a third thing.systemctl --user cat web.serviceis the command worth remembering, because it prints what the generator actually produced - into/run/user/1000/systemd/generator/, which is tmpfs. The unit does not exist on disk; it is rebuilt from your.containerfile on every reload.Read three lines of the generated output:
ExecStart=/usr/bin/podman run --name quadlet-web --replace --rm --cgroups=split --sdnotify=conmon -d --publish 8090:80 ... Type=notify NotifyAccess=all--sdnotify=conmonwithType=notifyis the interesting one. systemd will not consider the service started until something tells it so, and that something is conmon - the per-container monitor from guide 2. The monitor that exists because there is no daemon is the same process that reports readiness to the init system.Also note
ExecStopandExecStopPostboth runpodman rm -f, and theExecStartcarries--rm. A Quadlet container is recreated on every start, not restarted - so anything not in a volume is gone on restart, by design.bash Example session systemctl --user daemon-reloadsystemctl --user list-unit-files 'web*'UNIT FILE STATE PRESETweb.service generated - 1 unit files listed.systemctl --user cat web.service# /run/user/1000/systemd/generator/web.service# Automatically generated by /usr/lib/systemd/user-generators/podman-user-generator#[Unit]Wants=podman-user-wait-network-online.serviceAfter=podman-user-wait-network-online.serviceDescription=nginx, supervised by systemdSourcePath=/home/sysadmin/.config/containers/systemd/web.containerRequiresMountsFor=%t/containers [X-Container]Image=docker.io/library/nginx:alpinePublishPort=8090:80ContainerName=quadlet-web [Service]Restart=alwaysEnvironment=PODMAN_SYSTEMD_UNIT=%nKillMode=mixedExecStop=/usr/bin/podman rm -v -f -i quadlet-webExecStopPost=-/usr/bin/podman rm -v -f -i quadlet-webDelegate=yesType=notifyNotifyAccess=allSyslogIdentifier=%NExecStart=/usr/bin/podman run --name quadlet-web --replace --rm --cgroups=split --sdnotify=conmon -d --publish 8090:80 docker.io/library/nginx:alpine [Install]WantedBy=default.targetExpected resultState
generated, and a unit under/run/user/1000/systemd/generator/containing a fullpodman runcommand.Success conditionYou can read the command systemd will run without having written it.
-
Start it, and hand over ownership
systemctl --user start web.service, and the container appears inpodman psunder the name fromContainerName=.curlgetshttp=200.Now the demonstration that matters. Stop the container behind systemd's back:
$ podman stop quadlet-web quadlet-web $ sleep 3 $ podman ps quadlet-web Up 3 secondsIt came back, and the service never left
active.Restart=alwaysdid that - systemd saw its process exit and started it again.This is the operational shift to internalise: the container is no longer yours to stop.
podman stopis now a request that systemd will override. To actually stop it you usesystemctl --user stop web.service, and that applies to every Podman command that ends a container's life.It is also the answer to "how do I make my container come back after it crashes, with no daemon". Podman's own
--restartflag exists, but a Quadlet unit gets systemd's whole vocabulary: backoff, rate limiting, dependency ordering andAfter=on other units.bash Example session systemctl --user start web.servicesystemctl --user is-active web.serviceactivepodman ps --format 'table {{.Names}} {{.Status}} {{.Ports}}'NAMES STATUS PORTSquadlet-web Up 1 second 0.0.0.0:8090->80/tcpcurl -s -o /dev/null -w 'http=%{http_code}\n' http://localhost:8090http=200podman stop quadlet-webquadlet-websleep 3podman ps --format 'table {{.Names}} {{.Status}}'NAMES STATUSquadlet-web Up 3 secondsExpected resultAn active service, a serving container, and the container back within three seconds of being stopped.
Success conditionYou stopped a container and it restarted itself without you.
-
You cannot enable it, and you do not need to
The ordinary systemd habit fails here:
$ systemctl --user enable web.service Failed to enable unit: Unit /run/user/1000/systemd/generator/web.service is transient or generatedenableworks by creating a symlink into a.wants/directory for a unit file on disk. There is no file on disk to link to - the unit lives in tmpfs and is regenerated every reload.is-enabledanswersgenerated, which is systemd telling you the question does not apply.The
[Install] WantedBy=default.targetline in your.containerfile is what does the jobenablewould have done - the generator reads it and wires the dependency itself. Enablement is a property of your source file, not something you apply afterwards.So the whole workflow is: edit the
.containerfile,daemon-reload,restart. There is no separate install step, and there is no unit file to keep in sync with anything.bash Example session systemctl --user enable web.serviceFailed to enable unit: Unit /run/user/1000/systemd/generator/web.service is transient or generated[exit 1]systemctl --user is-enabled web.servicegeneratedExpected result
enablefailing withtransient or generated, andis-enabledreportinggenerated.Success conditionYou know which file to edit to make a Quadlet service start on its own.
Troubleshooting
Unit web.service not foundafter writing the file.Why: No
daemon-reload, a filename without a recognised extension, or the wrong directory.Fix:The extension must be one of
.container,.pod,.volume,.network,.kube,.imageor.build. Run the generator by hand to see its complaints:/usr/libexec/podman/quadlet -dryrun -user.The service fails immediately with no useful message.
Why: The container itself is failing, and
systemctl statusshows the unit's view rather than the container's.Fix:
journalctl --user -u web.service -n 50. Podman logs to journald by default, so the container's own output is in the same place as the unit's.Data written by the container disappears every restart.
Why: Quadlet's generated
ExecStartincludes--rmand the container is recreated rather than restarted.Fix:Working as designed. Put anything that must survive in a
Volume=, which is a first-class Quadlet key.podman stopappears not to work.Why:
Restart=alwaysis putting it back.Fix:
systemctl --user stop web.service. Use systemd's verbs for a systemd-owned container.