CertGrid CertGrid
Hands-on Lab·Podman

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

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.

Every command on this page ran on podman01.
Server NameIP AddressOSRolesCPURAMHDD
PODMAN01192.168.0.24Ubuntu 26.04 LTSPrimary Container Host2 Core4 GB50 GB

Before you start

  1. 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 every daemon-reload to 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/systemd

    Expected resultThe generator binary, and an empty unit directory.

    Success conditionYou know which directory systemd will read and which generator reads it.

  2. 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= and ContainerName= are first-class keys, not flags in a command line.
    • [Service] is ordinary systemd. Restart=always is systemd's restart policy, not Podman's - and this is the point of the whole track.
    • [Install] with WantedBy=default.target is what makes it start automatically. Remember this line; step 3 shows why it matters.

    Nothing here is a podman run invocation. 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.target

    Expected resultA nine-line unit file with [Container], [Service] and [Install].

    Success conditionThe file exists and names an image and a port.

  3. daemon-reload writes the unit

    systemctl --user daemon-reload runs the generator. list-unit-files then shows web.service with state generated - not enabled, not disabled, a third thing.

    systemctl --user cat web.service is 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 .container file 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=conmon with Type=notify is 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 ExecStop and ExecStopPost both run podman rm -f, and the ExecStart carries --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.target

    Expected resultState generated, and a unit under /run/user/1000/systemd/generator/ containing a full podman run command.

    Success conditionYou can read the command systemd will run without having written it.

  4. Start it, and hand over ownership

    systemctl --user start web.service, and the container appears in podman ps under the name from ContainerName=. curl gets http=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 seconds

    It came back, and the service never left active. Restart=always did 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 stop is now a request that systemd will override. To actually stop it you use systemctl --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 --restart flag exists, but a Quadlet unit gets systemd's whole vocabulary: backoff, rate limiting, dependency ordering and After= 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 seconds

    Expected 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.

  5. 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 generated

    enable works 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-enabled answers generated, which is systemd telling you the question does not apply.

    The [Install] WantedBy=default.target line in your .container file is what does the job enable would 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 .container file, 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.servicegenerated

    Expected resultenable failing with transient or generated, and is-enabled reporting generated.

    Success conditionYou know which file to edit to make a Quadlet service start on its own.

Troubleshooting

Official sources