CertGrid CertGrid
Hands-on Lab·Podman

Podman Pod Restart with systemd

A `.pod`, a `.volume` and a `.container` file, and starting one of them starts all three. Then `systemctl restart` on the container unit fails with `Bound to unit app-pod.service, but unit isn't active` - and takes the pod down with it.

systemd and Quadlet Guide 24 of 47 Advanced

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. Three file types, one application

    Quadlet has a file type per Podman object. Three of them here:

    • appdata.volume with [Volume] VolumeName=appdata - a named volume
    • app.pod with [Pod] PodName=app and PublishPort=8091:80 - note the port is on the pod, exactly as guide 21 showed it must be
    • app-web.container which joins them with Pod=app.pod and Volume=appdata.volume:/usr/share/nginx/html:z

    The references are filenames, not object names. Pod=app.pod names the file, and the generator turns that into a dependency on the unit that file produces. That is what lets it order things correctly without you writing After= anywhere.

    Setting VolumeName= and PodName= explicitly is worth the two lines. Without them Quadlet prefixes the object with systemd-, so appdata.volume would create a volume called systemd-appdata - which is findable but surprising the first time you go looking for it.

    bash Example session
    cat > ~/.config/containers/systemd/app.pod <<'UNIT'[Pod]PodName=appPublishPort=8091:80UNITcat > ~/.config/containers/systemd/app-web.container <<'UNIT'[Unit]Description=web, inside the app pod [Container]Image=docker.io/library/nginx:alpineContainerName=app-webPod=app.podVolume=appdata.volume:/usr/share/nginx/html:z [Install]WantedBy=default.targetUNITls ~/.config/containers/systemd/app-web.containerapp.podappdata.volumemyapp.containerweb.container

    Expected resultFour unit files in the directory, including the ones from earlier guides.

    Success conditionA pod, a volume and a container are each described by their own file.

  2. One start brings up three units

    daemon-reload, then start only the container unit.

    is-active on both reports active for the pod service as well - you did not start it. podman pod ps shows the pod running with two containers, and podman volume ls shows appdata created.

    The generator wired all of it. Look at what it put in After=:

    app-pod.service  appdata-volume.service  app.slice  basic.target

    The pod and the volume both have to exist before the container can join them, and systemd was told so. This is the argument for Quadlet over a shell script: dependency ordering is a solved problem in systemd, and expressing containers as units gets you the solution rather than a sequence of commands with sleep in it.

    bash Example session
    systemctl --user daemon-reloadsystemctl --user list-unit-files 'app*'UNIT FILE                                        STATE     PRESETapp-pod.service                                  generated -app-snap\x2duserd\x2dautostart@autostart.service generated -app-web.service                                  generated -appdata-volume.service                           generated -app.slice                                        static    - 5 unit files listed.systemctl --user start app-web.servicesystemctl --user is-active app-pod.service app-web.serviceactiveactivepodman pod psPOD ID        NAME        STATUS      CREATED                 INFRA ID      # OF CONTAINERSd51b5c5e7c31  app         Running     Less than a second ago  564a67815f03  2podman volume lsDRIVER      VOLUME NAMElocal       28510849432f70bcfae53e60894030b8b8fe5ffe4bee9ba10dc4ff2ba504367alocal       appdata

    Expected resultThree generated units, two active services, a running pod and an appdata volume.

    Success conditionStarting one unit produced a pod, a volume and a container.

  3. The volume is a real volume

    Write a file through the container into the mounted volume, and curl the published port - which is on the pod, not the container.

    served from a quadlet volume comes back, and podman volume inspect shows the data living at ~/.local/share/containers/storage/volumes/appdata/_data - inside your own rootless store, as guide 14 would predict.

    The :z suffix on the Volume= line is an SELinux relabel. It does nothing on Ubuntu, which uses AppArmor, and it is harmless - worth leaving in if the unit might ever run on RHEL or Fedora, where without it the container cannot read the volume at all.

    bash Example session
    podman exec app-web sh -c 'echo "served from a quadlet volume" > /usr/share/nginx/html/index.html'curl -s http://localhost:8091served from a quadlet volumepodman volume inspect appdata --format 'mount={{.Mountpoint}}'mount=/home/sysadmin/.local/share/containers/storage/volumes/appdata/_data

    Expected resultThe written line served over HTTP, and a mountpoint in your rootless store.

    Success conditionData written inside the container is served through the pod's port.

  4. Now restart it the obvious way, and watch it break

    The instinct is systemctl --user restart app-web.service. It fails:

    A dependency job for app-web.service failed. See 'journalctl -xe' for details.

    And it is worse than a failed command - curl now exits 7 and podman ps shows no app containers at all. The pod is gone. One restart took down the whole application.

    The journal names the cause exactly:

    app-web.service: Bound to unit app-pod.service, but unit isn't active.
    Dependency failed for app-web.service

    BindsTo=app-pod.service. Stopping the container triggers the pod unit's own stop, which removes the pod - and by the time systemd tries to start the container again, the pod it is bound to is mid-teardown and not active. The start is refused, and nothing brings the pod back because the thing that would have is the job that just failed.

    This is a genuine sharp edge, not a misconfiguration. It follows from a pod being a container's container: there is no meaningful way to restart one member of a pod whose lifecycle owns it.

    bash Example session
    systemctl --user restart app-web.serviceA dependency job for app-web.service failed. See 'journalctl -xe' for details.[exit 1]curl -s http://localhost:8091served from a quadlet volumepodman ps --format 'table {{.Names}} {{.Status}}'NAMES        STATUSquadlet-web  Up 4 minutesregistry     Up About a minutemyapp        Up 56 seconds

    Expected resultA dependency failure, a refused connection, and no app containers running.

    Success conditionYou have reproduced the failure and can name the directive that causes it.

  5. Restart the pod instead

    Confirm the wiring first - BindsTo really is app-pod.service - then restart that:

    $ systemctl --user restart app-pod.service
    $ systemctl --user is-active app-pod.service app-web.service
    active
    active

    Both back. The pod is recreated, the container rejoins it, and curl returns served from a quadlet volume - the data survived, because it was in a volume and the volume unit was never part of the restart.

    So the operating rule for a Quadlet pod:

    • systemctl restart -pod.service to cycle the application
    • systemctl start -web.service to bring one member up when the pod is already running
    • never restart a container unit that has Pod=

    And the general lesson, which is the one worth carrying past this guide: Quadlet units are ordinary systemd units, so when one behaves strangely, systemctl show the dependency properties and read the journal. The answer was one BindsTo line, and no amount of reasoning about Podman would have found it.

    bash Example session
    systemctl --user show app-web.service --property=BindsTo --valueapp-pod.servicesystemctl --user restart app-pod.servicesystemctl --user is-active app-pod.service app-web.serviceactiveactivepodman ps --format 'table {{.Names}} {{.Status}}'NAMES        STATUSquadlet-web  Up 5 minutesregistry     Up About a minutemyapp        Up About a minuteapp-infra    Up 1 secondapp-web      Up 1 secondcurl -s http://localhost:8091served from a quadlet volume

    Expected resultBindsTo=app-pod.service, both units active again, and the volume's content still served.

    Success conditionThe application is back and its data came with it.

Troubleshooting

Official sources