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
- OSUbuntu 26.04 LTS (resolute)
- Podman5.7.0
- Runtimecrun 1.21
- Networknetavark 1.16.1
- TimeAbout 15 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
- guide 40 - auto-update only works on containers owned by a systemd unit.
- A registry it can pull from. This guide runs one locally, which is enough to see the whole mechanism.
-
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=falseworks for a manual push, butauto-updateruns 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 = trueA 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.
-
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
myappin the catalogue.Success conditionThe registry holds an image your Quadlet unit can reference.
-
One key turns it on
The unit is the one from guide 40 plus a single line:
AutoUpdate=registryStart 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=registryQuadlet's
AutoUpdate=key became the container labelio.containers.autoupdate. That is all the key does - the label is the real interface, and you could set it by hand with--labelon a plainpodman 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=registryExpected result
version onein the logs, a digest, andautoupdate=registry.Success conditionA running container is labelled for auto-update and you know its digest.
-
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 formyapp:latest.podman auto-update --dry-runreports 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 pendingpending- 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 sayversion TWO, and the digest has changed from7f82ae2c9906...to0870103a0e0a....Nothing orchestrated that. There is no controller, no cluster and no daemon -
auto-updatecompared a digest, pulled an image, and asked systemd to restart the unit. Which is whyRestart=alwaysand 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.timerruns 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-runin 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=registryExpected result
pendingfrom the dry run,truefrom the real one,version TWOin the logs and a different digest.Success conditionThe container is running an image it pulled by itself.
Troubleshooting
auto-updatereports nothing to do although you pushed a new image.Why: The container is not labelled, or it is not owned by a systemd unit. Both are required.
Fix:
podman inspect <c> --format '{{index .Config.Labels "io.containers.autoupdate"}}'and{{index .Config.Env}}- the latter must containPODMAN_SYSTEMD_UNIT.pinging container registry localhost:5000: ... http: server gave HTTP response to HTTPS client.Why: The registry is plain HTTP and nothing has declared it insecure.
Fix:The
[[registry]]block withinsecure = true. A--tls-verify=falseflag cannot help, because auto-update passes no flags.The image is pulled but the container keeps running the old one.
Why: The restart failed, so auto-update rolled back to the previous image - which is deliberate behaviour.
Fix:
journalctl --user -u myapp.servicefor why the new image would not start. Auto-update rolling back on a failed start is a feature worth knowing about.You want this on a digest-pinned image.
Why: Auto-update compares the digest behind a tag. A reference that is already a digest can never change.
Fix:Use a tag. If you want pinning, you do not want auto-update - pick one.