CertGrid CertGrid
Hands-on Lab·Podman

Podman Auto-Update with systemd

Push a new image under the same tag and `podman auto-update` pulls it and restarts the container - no orchestrator. Verified end to end against a registry running on the same host: `version one` became `version TWO` and the digest changed.

systemd and Quadlet Guide 23 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. A registry, and one line of configuration

    Auto-update needs somewhere to check, so run a registry on the host.

    It also needs that registry to be reachable without TLS, and this is where people get stuck: --tls-verify=false works for a manual push, but auto-update runs from a systemd unit with no flags of yours. It reads configuration, so the configuration is what has to say it:

    [[registry]]
    location = "localhost:5000"
    insecure = true

    A file in /etc/containers/registries.conf.d/ - the same directory that decided short names in guide 12.

    The empty catalogue confirms the registry is answering and has nothing in it yet.

    bash Example session
    sudo -n tee /etc/containers/registries.conf.d/98-local.conf <<'CONF'[[registry]]location = "localhost:5000"insecure = trueCONF[[registry]]location = "localhost:5000"insecure = truepodman run -d --name registry -p 5000:5000 docker.io/library/registry:3Trying to pull docker.io/library/registry:3...Getting image source signaturesCopying blob sha256:8f4e1177a675ecd1759cde12c405d158ff166a809659e4006966a2b1a7c87123Copying blob sha256:e6f31ffc071e5560b82a8685fba8214954e5721e3e49269d00958316edbe89feCopying blob sha256:269b60c1a347a7b2501df94ec2391af4c325bb9ef5a0cf5a4e6dd2e87198eb21Copying blob sha256:47465c9fe4b1438b5f9aed1cf7182de26087f6eda66eb23dce944b763673bbedCopying blob sha256:f90c3e90567750f85e63c5ba884e9ef81f4839ab404a8f3dfd306e661018c6a0Copying config sha256:1bc3f1a4432290d5ac3341603f15bf99c5b2feb4a9e3195e530aadc66fbcc65eWriting manifest to image destinationb96bdbe2b5cd6166c8f8d7fa82b783264d400b6929cf079971c14f40023df496curl -s http://localhost:5000/v2/_catalog{"repositories":[]}

    Expected resultThe config file echoed back, a running registry, and {"repositories":[]}.

    Success conditionA registry is serving on localhost:5000 and Podman is willing to talk to it without TLS.

  2. Push version one

    A two-instruction image whose only job is to say which version it is. Build it, tag it localhost:5000/myapp:latest, push it.

    Note the tag is latest, and that it will not change for the rest of this guide. Auto-update works by comparing digests behind a fixed tag, so a moving tag is the mechanism rather than a bad habit here - though it is worth being clear that this is the one place a moving tag is the point.

    The catalogue now lists myapp.

    bash Example session
    mkdir -p ~/au && cd ~/au && cat > Containerfile <<'CF'FROM docker.io/library/alpine:3.22RUN echo "version one" > /versionCMD ["sh", "-c", "cat /version; sleep 3600"]CFcd ~/au && podman build -t localhost:5000/myapp:latest .STEP 1/3: FROM docker.io/library/alpine:3.22STEP 2/3: RUN echo "version one" > /version--> f9f558b6dce5STEP 3/3: CMD ["sh", "-c", "cat /version; sleep 3600"]COMMIT localhost:5000/myapp:latest--> b92795baa673Successfully tagged localhost:5000/myapp:latestb92795baa673585ea4a75c601e4d722e8198885f41845a7b3a9519a38f0d6d80podman push localhost:5000/myapp:latestGetting image source signaturesCopying blob sha256:0e0b466f52679040b73603bae2e40b26bca3877b2b398548aae2552d52fc6920Copying blob sha256:6f09edfb3f6d7173733adc8eec8ea00626550dc6fc2dcf07d40e13f5c1e907c4Copying config sha256:b92795baa673585ea4a75c601e4d722e8198885f41845a7b3a9519a38f0d6d80Writing manifest to image destinationcurl -s http://localhost:5000/v2/_catalog{"repositories":[]}

    Expected resultA built image, a successful push, and myapp in the catalogue.

    Success conditionThe registry holds an image your Quadlet unit can reference.

  3. One key turns it on

    The unit is the one from guide 40 plus a single line:

    AutoUpdate=registry

    Start it and it logs version one.

    Two fields are worth reading back. The digest - sha256:7f82ae2c9906... - is the exact image this container is running, and it is what auto-update will compare against. And the label:

    autoupdate=registry

    Quadlet's AutoUpdate= key became the container label io.containers.autoupdate. That is all the key does - the label is the real interface, and you could set it by hand with --label on a plain podman run. Quadlet just gives it a name that reads like configuration.

    The other accepted value is local, which watches your local image store instead of a registry - useful when you build on the same host and never push.

    bash Example session
    cat > ~/.config/containers/systemd/myapp.container <<'UNIT'[Unit]Description=myapp, following the latest tag [Container]Image=localhost:5000/myapp:latestContainerName=myappAutoUpdate=registry [Service]Restart=always [Install]WantedBy=default.targetUNITsystemctl --user daemon-reloadsystemctl --user start myapp.servicepodman logs myappversion onepodman inspect myapp --format 'digest={{.ImageDigest}} autoupdate={{index .Config.Labels "io.containers.autoupdate"}}'digest=sha256:7f82ae2c9906f4aa7f76bb558fbf6bace0f30955da30746a6a2693f04172bad7 autoupdate=registry

    Expected resultversion one in the logs, a digest, and autoupdate=registry.

    Success conditionA running container is labelled for auto-update and you know its digest.

  4. Move the tag, then update

    Change the image so it says version TWO, build, and push to the same tag. The registry now serves different bytes for myapp:latest.

    podman auto-update --dry-run reports what it would do, and this is the command to put in a change window:

    UNIT           CONTAINER             IMAGE                        POLICY    UPDATED
    myapp.service  c82799dc0640 (myapp)  localhost:5000/myapp:latest  registry  pending

    pending - a newer digest exists behind that tag.

    Then run it for real. It pulls, and the same table comes back with UPDATED: true. The container's logs now say version TWO, and the digest has changed from 7f82ae2c9906... to 0870103a0e0a....

    Nothing orchestrated that. There is no controller, no cluster and no daemon - auto-update compared a digest, pulled an image, and asked systemd to restart the unit. Which is why Restart=always and a volume for anything that matters are not optional here: the container is recreated, exactly as guide 40 showed.

    In production you would not run this by hand. Podman ships podman-auto-update.timer - systemctl --user enable --now podman-auto-update.timer runs it daily. Whether you want a container that silently changes version overnight is a real decision: it is excellent for a security-patched base image and alarming for an application, and --dry-run in a cron job that emails you is the middle ground.

    bash Example session
    mkdir -p ~/au && cd ~/au && cat > Containerfile <<'CF'FROM docker.io/library/alpine:3.22RUN echo "version one" > /versionCMD ["sh", "-c", "cat /version; sleep 3600"]CFpodman push localhost:5000/myapp:latestGetting image source signaturesCopying blob sha256:0e0b466f52679040b73603bae2e40b26bca3877b2b398548aae2552d52fc6920Copying blob sha256:6f09edfb3f6d7173733adc8eec8ea00626550dc6fc2dcf07d40e13f5c1e907c4Copying config sha256:b92795baa673585ea4a75c601e4d722e8198885f41845a7b3a9519a38f0d6d80Writing manifest to image destinationpodman auto-update --dry-run            UNIT           CONTAINER             IMAGE                        POLICY      UPDATED            myapp.service  c82799dc0640 (myapp)  localhost:5000/myapp:latest  registry    pendingpodman auto-update --dry-run            UNIT           CONTAINER             IMAGE                        POLICY      UPDATED            myapp.service  c82799dc0640 (myapp)  localhost:5000/myapp:latest  registry    pendingpodman logs myappversion onepodman inspect myapp --format 'digest={{.ImageDigest}} autoupdate={{index .Config.Labels "io.containers.autoupdate"}}'digest=sha256:7f82ae2c9906f4aa7f76bb558fbf6bace0f30955da30746a6a2693f04172bad7 autoupdate=registry

    Expected resultpending from the dry run, true from the real one, version TWO in the logs and a different digest.

    Success conditionThe container is running an image it pulled by itself.

Troubleshooting

Official sources