Tools: Complete Guide to Stop Babysitting Container Updates: Practical Podman Auto-Updates with Quadlet, Health Checks, and Rollback

Tools: Complete Guide to Stop Babysitting Container Updates: Practical Podman Auto-Updates with Quadlet, Health Checks, and Rollback

What Podman auto-update actually does

Why Quadlet is the easiest way to do this

Example: a rootless Quadlet with auto-update enabled

A more realistic readiness + health-check pattern

Containerfile

app.py

entrypoint.sh

Test before you trust it

Change the schedule instead of accepting midnight

Registry auth matters for private images

Common mistakes that break auto-updates

1) Using a short image name

2) Running the container outside systemd

3) Trusting latest without a rollback path

4) No health check

A quick verification checklist

When to use local instead of registry

Final take If you run long-lived containers on Linux, "just pull the new image and restart it later" usually turns into "I'll do it this weekend". That is how drift sneaks in. Podman already has a cleaner answer. Its auto-update flow can check for a new image, pull it, and restart the corresponding systemd unit. Better yet, it can roll back if the restart fails. The catch is that you need to wire it up the right way. In practice, that means: Here is a practical setup for a rootless container managed with Quadlet. According to podman-auto-update(1), Podman can update containers that run inside systemd units. It checks containers marked for auto-update, pulls a newer image when available, and restarts the unit that owns the container. It supports two policies: For most people running pulled images, registry is the useful one. One important limitation from the docs: registry requires a fully qualified image name like docker.io/library/nginx:1.27-alpine or quay.io/yourorg/app:latest. A short name is not enough. Quadlet lets you define Podman workloads as .container files that systemd turns into regular services at daemon reload time. Podman documents rootless Quadlet search paths such as: That makes it a good fit for auto-updates, because Podman can restart the generated systemd service after pulling a new image. Create the Quadlet directory if needed: Now create ~/.config/containers/systemd/whoami.container: Then load and start it: Verify that it is running: The quick example above proves the wiring, but it does not give systemd much insight into application health. Rollback works best when systemd can tell whether the new container actually became ready. Podman documents that podman auto-update --rollback is most reliable when the container sends the READY=1 notification through sdnotify. For Quadlet, Notify=true maps to --sdnotify container. That means your application should emit readiness only when it is genuinely ready to serve traffic. One straightforward pattern is a small wrapper entrypoint. And the matching Quadlet: This gives you two useful signals: That combination is much safer than "container process started, so I guess the deploy worked". Before enabling unattended updates, do a dry run: Or format the output to focus on what matters: If Podman sees a newer image, the Updated field shows pending in dry-run mode. You can trigger an update manually as a controlled test: Then inspect what happened: Podman ships podman-auto-update.timer, and the docs say it triggers daily at midnight by default. If that is a bad maintenance window for you, override the timer instead of editing vendor files in place: Why the empty OnCalendar= first? In systemd drop-ins, that clears the original value before you set a new one. Persistent=true is useful on machines that are not always on, because missed runs get caught up the next time the timer becomes active. podman-auto-update(1) documents that registry auth is read from the normal Podman auth file path, typically ${XDG_RUNTIME_DIR}/containers/auth.json on Linux, with $HOME/.docker/config.json as a fallback. So if your image is private, log in first as the same user that owns the rootless service: If you need a non-default auth file, the docs also support: This often fails for registry updates: Use a fully qualified reference instead: podman auto-update updates the systemd unit that owns the container. If you started the container with an ad hoc podman run -d ..., there is no systemd unit for Podman to restart. If you want automatic pulls, automatic rollback is not optional in spirit, even though it is enabled by default in podman auto-update. Pair it with readiness notifications so Podman can tell the difference between "started" and "working". A process can stay alive while the application is unusable. HealthCmd= and friends give you an ongoing signal after startup. After setup, I like to verify these points: You should confirm that: local is useful when another workflow places newer images into local storage first, for example: In that model, podman auto-update becomes a restart controller instead of a registry poller. Podman auto-updates are good, but they become genuinely production-friendly when you add the missing pieces around them: That gets you much closer to "safe unattended updates" instead of "automatic surprises". Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ mkdir -p ~/.config/containers/systemd mkdir -p ~/.config/containers/systemd mkdir -p ~/.config/containers/systemd [Unit] Description=Traefik whoami demo container After=network-online.target Wants=network-online.target [Container] ContainerName=whoami Image=-weight: 500;">docker.io/traefik/whoami:v1.10.1 AutoUpdate=registry PublishPort=127.0.0.1:8080:80 [Service] Restart=always RestartSec=5 TimeoutStartSec=180 [Install] WantedBy=default.target [Unit] Description=Traefik whoami demo container After=network-online.target Wants=network-online.target [Container] ContainerName=whoami Image=-weight: 500;">docker.io/traefik/whoami:v1.10.1 AutoUpdate=registry PublishPort=127.0.0.1:8080:80 [Service] Restart=always RestartSec=5 TimeoutStartSec=180 [Install] WantedBy=default.target [Unit] Description=Traefik whoami demo container After=network-online.target Wants=network-online.target [Container] ContainerName=whoami Image=-weight: 500;">docker.io/traefik/whoami:v1.10.1 AutoUpdate=registry PublishPort=127.0.0.1:8080:80 [Service] Restart=always RestartSec=5 TimeoutStartSec=180 [Install] WantedBy=default.target -weight: 500;">systemctl --user daemon-reload -weight: 500;">systemctl --user -weight: 500;">enable --now whoami.-weight: 500;">service -weight: 500;">systemctl --user daemon-reload -weight: 500;">systemctl --user -weight: 500;">enable --now whoami.-weight: 500;">service -weight: 500;">systemctl --user daemon-reload -weight: 500;">systemctl --user -weight: 500;">enable --now whoami.-weight: 500;">service -weight: 500;">systemctl --user -weight: 500;">status whoami.-weight: 500;">service podman ps --filter name=whoami -weight: 500;">curl -fsS http://127.0.0.1:8080 -weight: 500;">systemctl --user -weight: 500;">status whoami.-weight: 500;">service podman ps --filter name=whoami -weight: 500;">curl -fsS http://127.0.0.1:8080 -weight: 500;">systemctl --user -weight: 500;">status whoami.-weight: 500;">service podman ps --filter name=whoami -weight: 500;">curl -fsS http://127.0.0.1:8080 FROM python:3.12-slim RUN -weight: 500;">apt-get -weight: 500;">update \ && -weight: 500;">apt-get -weight: 500;">install -y --no--weight: 500;">install-recommends -weight: 500;">curl systemd \ && rm -rf /var/lib/-weight: 500;">apt/lists/* RUN -weight: 500;">pip -weight: 500;">install --no-cache-dir flask WORKDIR /app COPY app.py /app/app.py COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh EXPOSE 8000 CMD ["/app/entrypoint.sh"] FROM python:3.12-slim RUN -weight: 500;">apt-get -weight: 500;">update \ && -weight: 500;">apt-get -weight: 500;">install -y --no--weight: 500;">install-recommends -weight: 500;">curl systemd \ && rm -rf /var/lib/-weight: 500;">apt/lists/* RUN -weight: 500;">pip -weight: 500;">install --no-cache-dir flask WORKDIR /app COPY app.py /app/app.py COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh EXPOSE 8000 CMD ["/app/entrypoint.sh"] FROM python:3.12-slim RUN -weight: 500;">apt-get -weight: 500;">update \ && -weight: 500;">apt-get -weight: 500;">install -y --no--weight: 500;">install-recommends -weight: 500;">curl systemd \ && rm -rf /var/lib/-weight: 500;">apt/lists/* RUN -weight: 500;">pip -weight: 500;">install --no-cache-dir flask WORKDIR /app COPY app.py /app/app.py COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh EXPOSE 8000 CMD ["/app/entrypoint.sh"] from flask import Flask app = Flask(__name__) @app.get("/healthz") def healthz(): return {"ok": True} @app.get("/") def index(): return "hello from podman auto--weight: 500;">update\n" if __name__ == "__main__": app.run(host="0.0.0.0", port=8000) from flask import Flask app = Flask(__name__) @app.get("/healthz") def healthz(): return {"ok": True} @app.get("/") def index(): return "hello from podman auto--weight: 500;">update\n" if __name__ == "__main__": app.run(host="0.0.0.0", port=8000) from flask import Flask app = Flask(__name__) @app.get("/healthz") def healthz(): return {"ok": True} @app.get("/") def index(): return "hello from podman auto--weight: 500;">update\n" if __name__ == "__main__": app.run(host="0.0.0.0", port=8000) #!/bin/sh set -eu python /app/app.py & pid=$! for _ in $(seq 1 30); do if -weight: 500;">curl -fsS http://127.0.0.1:8000/healthz >/dev/null; then systemd-notify --ready wait "$pid" exit $? fi sleep 1 done echo "application failed readiness check" >&2 kill "$pid" wait "$pid" || true exit 1 #!/bin/sh set -eu python /app/app.py & pid=$! for _ in $(seq 1 30); do if -weight: 500;">curl -fsS http://127.0.0.1:8000/healthz >/dev/null; then systemd-notify --ready wait "$pid" exit $? fi sleep 1 done echo "application failed readiness check" >&2 kill "$pid" wait "$pid" || true exit 1 #!/bin/sh set -eu python /app/app.py & pid=$! for _ in $(seq 1 30); do if -weight: 500;">curl -fsS http://127.0.0.1:8000/healthz >/dev/null; then systemd-notify --ready wait "$pid" exit $? fi sleep 1 done echo "application failed readiness check" >&2 kill "$pid" wait "$pid" || true exit 1 [Container] ContainerName=demo-api Image=-weight: 500;">docker.io/yourname/demo-api:1.0.0 AutoUpdate=registry Notify=true PublishPort=127.0.0.1:8000:8000 HealthCmd=-weight: 500;">curl -fsS http://127.0.0.1:8000/healthz || exit 1 HealthInterval=30s HealthTimeout=5s HealthRetries=3 HealthOnFailure=kill [Service] Restart=always TimeoutStartSec=180 [Install] WantedBy=default.target [Container] ContainerName=demo-api Image=-weight: 500;">docker.io/yourname/demo-api:1.0.0 AutoUpdate=registry Notify=true PublishPort=127.0.0.1:8000:8000 HealthCmd=-weight: 500;">curl -fsS http://127.0.0.1:8000/healthz || exit 1 HealthInterval=30s HealthTimeout=5s HealthRetries=3 HealthOnFailure=kill [Service] Restart=always TimeoutStartSec=180 [Install] WantedBy=default.target [Container] ContainerName=demo-api Image=-weight: 500;">docker.io/yourname/demo-api:1.0.0 AutoUpdate=registry Notify=true PublishPort=127.0.0.1:8000:8000 HealthCmd=-weight: 500;">curl -fsS http://127.0.0.1:8000/healthz || exit 1 HealthInterval=30s HealthTimeout=5s HealthRetries=3 HealthOnFailure=kill [Service] Restart=always TimeoutStartSec=180 [Install] WantedBy=default.target podman auto--weight: 500;">update --dry-run podman auto--weight: 500;">update --dry-run podman auto--weight: 500;">update --dry-run podman auto--weight: 500;">update --dry-run --format '{{.Unit}} {{.Image}} {{.Updated}}' podman auto--weight: 500;">update --dry-run --format '{{.Unit}} {{.Image}} {{.Updated}}' podman auto--weight: 500;">update --dry-run --format '{{.Unit}} {{.Image}} {{.Updated}}' -weight: 500;">systemctl --user -weight: 500;">start podman-auto--weight: 500;">update.-weight: 500;">service -weight: 500;">systemctl --user -weight: 500;">start podman-auto--weight: 500;">update.-weight: 500;">service -weight: 500;">systemctl --user -weight: 500;">start podman-auto--weight: 500;">update.-weight: 500;">service journalctl --user -u podman-auto--weight: 500;">update.-weight: 500;">service -n 100 --no-pager journalctl --user -u whoami.-weight: 500;">service -n 100 --no-pager journalctl --user -u podman-auto--weight: 500;">update.-weight: 500;">service -n 100 --no-pager journalctl --user -u whoami.-weight: 500;">service -n 100 --no-pager journalctl --user -u podman-auto--weight: 500;">update.-weight: 500;">service -n 100 --no-pager journalctl --user -u whoami.-weight: 500;">service -n 100 --no-pager mkdir -p ~/.config/systemd/user/podman-auto--weight: 500;">update.timer.d cat > ~/.config/systemd/user/podman-auto--weight: 500;">update.timer.d/override.conf <<'EOF' [Timer] OnCalendar= OnCalendar=Sat *-*-* 03:15:00 Persistent=true RandomizedDelaySec=15m EOF -weight: 500;">systemctl --user daemon-reload -weight: 500;">systemctl --user -weight: 500;">restart podman-auto--weight: 500;">update.timer -weight: 500;">systemctl --user list-timers podman-auto--weight: 500;">update.timer mkdir -p ~/.config/systemd/user/podman-auto--weight: 500;">update.timer.d cat > ~/.config/systemd/user/podman-auto--weight: 500;">update.timer.d/override.conf <<'EOF' [Timer] OnCalendar= OnCalendar=Sat *-*-* 03:15:00 Persistent=true RandomizedDelaySec=15m EOF -weight: 500;">systemctl --user daemon-reload -weight: 500;">systemctl --user -weight: 500;">restart podman-auto--weight: 500;">update.timer -weight: 500;">systemctl --user list-timers podman-auto--weight: 500;">update.timer mkdir -p ~/.config/systemd/user/podman-auto--weight: 500;">update.timer.d cat > ~/.config/systemd/user/podman-auto--weight: 500;">update.timer.d/override.conf <<'EOF' [Timer] OnCalendar= OnCalendar=Sat *-*-* 03:15:00 Persistent=true RandomizedDelaySec=15m EOF -weight: 500;">systemctl --user daemon-reload -weight: 500;">systemctl --user -weight: 500;">restart podman-auto--weight: 500;">update.timer -weight: 500;">systemctl --user list-timers podman-auto--weight: 500;">update.timer podman login -weight: 500;">docker.io podman login -weight: 500;">docker.io podman login -weight: 500;">docker.io Image=nginx:latest Image=nginx:latest Image=nginx:latest Image=-weight: 500;">docker.io/library/nginx:1.27-alpine Image=-weight: 500;">docker.io/library/nginx:1.27-alpine Image=-weight: 500;">docker.io/library/nginx:1.27-alpine -weight: 500;">systemctl --user cat whoami.-weight: 500;">service podman inspect whoami --format '{{.Config.Labels}}' podman auto--weight: 500;">update --dry-run --format '{{.Unit}} {{.Policy}} {{.Updated}}' -weight: 500;">systemctl --user -weight: 500;">status podman-auto--weight: 500;">update.timer -weight: 500;">systemctl --user list-timers podman-auto--weight: 500;">update.timer -weight: 500;">systemctl --user cat whoami.-weight: 500;">service podman inspect whoami --format '{{.Config.Labels}}' podman auto--weight: 500;">update --dry-run --format '{{.Unit}} {{.Policy}} {{.Updated}}' -weight: 500;">systemctl --user -weight: 500;">status podman-auto--weight: 500;">update.timer -weight: 500;">systemctl --user list-timers podman-auto--weight: 500;">update.timer -weight: 500;">systemctl --user cat whoami.-weight: 500;">service podman inspect whoami --format '{{.Config.Labels}}' podman auto--weight: 500;">update --dry-run --format '{{.Unit}} {{.Policy}} {{.Updated}}' -weight: 500;">systemctl --user -weight: 500;">status podman-auto--weight: 500;">update.timer -weight: 500;">systemctl --user list-timers podman-auto--weight: 500;">update.timer - run the container through a systemd unit - use a fully qualified image reference for registry-based updates - add a readiness signal so rollback can detect bad starts reliably - add a health check so broken containers do not look healthy by accident - registry, which checks the remote registry for a newer digest - local, which compares the container image to a newer image already present in local storage - ~/.config/containers/systemd/ - $XDG_RUNTIME_DIR/containers/systemd/ - systemd-notify --ready tells systemd the -weight: 500;">service really started - HealthCmd= keeps probing after startup and can kill the container if it becomes unhealthy - podman auto--weight: 500;">update --authfile /path/to/auth.json - the io.containers.autoupdate.authfile label - the REGISTRY_AUTH_FILE environment variable - the generated -weight: 500;">service exists - the container carries the auto--weight: 500;">update policy - dry run works cleanly - the timer is active on the schedule you expect - a CI job pre-pulls or pre-loads images - you import signed images into an offline host - you promote images between local stores before -weight: 500;">restart - Quadlet for clean systemd ownership - fully qualified image names - health checks - readiness notifications - a deliberate timer schedule - Podman documentation, podman-auto--weight: 500;">update(1): https://docs.podman.io/en/stable/markdown/podman-auto--weight: 500;">update.1.html - Podman documentation, podman-systemd.unit(5): https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html - Podman documentation, podman-container.unit(5): https://docs.podman.io/en/latest/markdown/podman-container.unit.5.html - systemd documentation, systemd.time(7): https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html - systemd documentation, systemd.timer(5): https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html