LinuxcontainersTested on real hardware

Check for Docker Image Updates Without Diun

A read-only shell script that flags Docker images with a newer registry digest than the one you run — writing one pending-updates JSON your daily digest can read, no Diun needed.

Shellsh
Updated
Script
sh
#!/bin/sh
# image-update-check.sh — list Docker images whose registry digest is newer
# than the one you're running, WITHOUT Diun. Read-only: it only INSPECTS,
# never pulls. Written for busybox sh so it runs directly on a NAS.
set -u

# ---- make these values your own ------------------------------------------
OUT="/var/lib/digest-feeds/40-image-updates.json"   # where to write the report
# Watched images — the EXACT tags you run, one per line (always include a tag):
IMAGES="
lscr.io/linuxserver/radarr:latest
ghcr.io/flaresolverr/flaresolverr:latest
nginx:1.27
"
# --------------------------------------------------------------------------

items=""; n=0
for img in $IMAGES; do
  [ -n "$img" ] || continue
  repo="${img%:*}"                       # strip only the :tag (keeps any :port)

  # Digest of the image we are RUNNING, as it was pulled from the registry.
  running="$(docker image inspect "$img" \
      --format '{{range .RepoDigests}}{{println .}}{{end}}' 2>/dev/null \
      | grep -F "${repo}@" | head -n1 | sed 's/^.*@//')"

  # The registry's CURRENT digest for that tag (needs the buildx plugin).
  # timeout guards against a hung registry lookup stalling the whole run.
  registry="$(timeout 20 docker buildx imagetools inspect "$img" \
      --format '{{.Manifest.Digest}}' 2>/dev/null)"

  # Skip anything we can't resolve on BOTH sides — never guess an update.
  [ -n "$running" ] && [ -n "$registry" ] || continue

  if [ "$running" != "$registry" ]; then
    obj="{\"image\":\"$img\",\"running\":\"$running\",\"registry\":\"$registry\"}"
    [ -z "$items" ] && items="$obj" || items="$items,$obj"
    n=$((n + 1))
  fi
done

mkdir -p "$(dirname "$OUT")"
printf '{"generated":"%s","pending":[%s]}\n' \
  "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$items" > "$OUT"
echo "wrote $OUT — $n image(s) pending"

What this does

I like knowing when a container I run has a newer image waiting — but I don’t want a notification every time a tag changes at 3am. What I actually want, once a day, is a plain answer to “which of my images are behind right now?” That’s a state, not a stream of events, and it’s the one thing a push-on-change tool isn’t built to hand you. So instead of running Diun (a great tool — more on that below), this little script asks Docker two questions per image: what digest am I running, and what digest does the registry serve for that tag right now? If they differ, an update is pending. It writes the pending set to one JSON file that a daily digest email or a dashboard can read. It only ever inspects — it never pulls — so it’s safe to run read-only against your Docker socket, even on a NAS.

Diun vs this: pick the model you want

Diun watches your images and notifies you on change — push, as it happens. That’s the right tool if you want to be told the moment something updates. This script answers a different question: what’s pending right now, as one snapshot in a file. It suits a once-a-day digest that gathers current state from several sources. Neither is better — they’re different shapes. If you want push-on-change, use Diun and stop reading here.

How the check works

The whole thing rests on one idea: an image tag like nginx:1.27 is a moving pointer, but every actual image underneath it has an immutable, content-addressable digest (sha256:…). “Am I up to date?” is just “does the digest I pulled still match the digest the registry serves for that tag?”

RUNNING (docker inspect)sha256:a1b2…9fREGISTRY (imagetools)sha256:c3d4…7ecompareimage-updates.json“pending”: [radarr:latest …

The running side comes from docker image inspect’s RepoDigests — Docker’s own docs describe these as “content-addressable digests of locally available image manifests that the image is referenced from,” recorded when the image was pulled from a registry. The registry side comes from docker buildx imagetools inspect … --format '{{.Manifest.Digest}}', which returns the current manifest-index digest the registry serves for that tag. For an image you pulled normally by its tag, both are the same kind of digest, so comparing them is apples-to-apples.

Prerequisites

  • Docker, with the buildx plugin (docker buildx version should succeed) — it ships with modern Docker Engine / Docker Desktop, and the script uses it for the registry lookup.
  • Read access to the Docker socket. Nothing here writes to Docker, so a read-only mount is enough.
  • Outbound network to your registries (Docker Hub, GHCR, LSCR, …) so the registry digest can be fetched.

Try it once

Set the IMAGES list to the exact tags you run, then run it and read the file it writes:

sh

sh image-update-check.sh
cat /var/lib/digest-feeds/40-image-updates.json

An empty "pending": [] means everything you watch is current. Any entry lists the image plus both digests, so you can see exactly what changed before you decide to pull.

Schedule it daily

Point cron at it once a day, before your digest runs, and send its stdout to a log rather than the mailer:

sh

# crontab: check image updates every day at 07:00
0 7 * * * /usr/local/bin/image-update-check.sh >> /var/log/image-updates.log 2>&1

Notes

  • Make these values your own. OUT is where the report lands (any path your digest or dashboard reads); the IMAGES list must be the exact tags you run, each with a tag (nginx:1.27, not bare nginx). Rule of thumb: if a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.
  • It never pulls. docker image inspect and docker buildx imagetools inspect are both read-only lookups — the script reports what’s pending and stops. Applying updates is a separate, deliberate docker compose pull && up -d you run when you choose.
  • Locally-built images are skipped, on purpose. An image you built yourself and never pushed has no RepoDigests, so there’s nothing registry-side to compare. The script skips anything it can’t resolve on both sides rather than guessing.
  • The digest edge case to know about. Both sides are the tag’s manifest-index digest for a normal docker pull <tag>. If you instead pulled an image by a platform-specific digest, its RepoDigests holds that narrower digest and could read as “pending” against the index digest — rare, but worth knowing before you trust a surprising result. When unsure, docker buildx imagetools inspect <img> shows the full picture.
  • Wire it into a digest. The JSON is designed to be one feed among many — the daily digest playbook reads files like this and renders an “updates available” card, so pending images show up in your morning email instead of a separate app.
  • What “tested” means here: the running-vs-registry digest comparison above is exactly the check that runs daily on my own NAS (Docker 25 with the buildx plugin) to feed that morning digest — it correctly reports which watched images are behind, and each command is verified against the Docker documentation. The code here is a cleaned-up skeleton of that job; set your own IMAGES and OUT and do a first run by hand before trusting it to cron.

Related reading: Daily digest email · Install Docker on Linux · Arr stack Docker Compose.

Sources: Docker image inspect (RepoDigests), docker buildx imagetools inspect, Diun.