Skip to content

fix(docker): clear the update-available flag after an out-of-band image update - #2723

Open
Junker der Provinz (junkerderprovinz) wants to merge 1 commit into
unraid:masterfrom
junkerderprovinz:fix/docker-stale-update-status
Open

fix(docker): clear the update-available flag after an out-of-band image update#2723
Junker der Provinz (junkerderprovinz) wants to merge 1 commit into
unraid:masterfrom
junkerderprovinz:fix/docker-stale-update-status

Conversation

@junkerderprovinz

@junkerderprovinz Junker der Provinz (junkerderprovinz) commented Aug 13, 2026

Copy link
Copy Markdown

Fixes #2710.

DockerUpdate::reloadUpdateStatus() read the local digest out of /var/lib/docker/unraid-update-status.json and only called inspectLocalVersion() when that entry was missing. Once an image had been checked, its cached digest was trusted forever. Replacing the image outside the Docker tab (a docker pull, or a recreate driven through the API or CLI) never refreshed it, so the tab stayed on "update available" and scripts/dockerupdate kept sending a version notification on every run. "Check for Updates" could not clear it either, because it reaches the same code through getAllInfo(true).

The fix is to inspect the local image on every check and keep the cached digest only as a fallback for images that carry no repo digest, such as locally built ones. The inspect is a local Docker socket call, so this costs nothing next to getRemoteVersionV2(), which the same loop already calls unconditionally.

Making the live inspect authoritative uncovered a second problem in inspectLocalVersion(). An image can carry several digests for the same repo, because a multiarch tag that was re-pushed keeps the older manifest list as well, and the tag does not always resolve to the last one. On my server redis:latest has two:

["redis@sha256:344e3945a0b4...","redis@sha256:39353c6a2f31..."]

and docker buildx imagetools inspect redis:latest resolves to sha256:344e3945a0b4, the first entry. Taking the last entry unconditionally (#2581) therefore reports a spurious update as soon as the digest is read live instead of from the cache. So inspectLocalVersion() now takes the digest the tag actually resolves to when the local image carries it, and keeps the last entry as the fallback when it does not. Membership is the right test here: if the registry's current digest is among the image's repo digests, the local image is what the tag points at.

inspectLocalVersion() gained an optional second parameter, so existing one-argument callers are unaffected.

Verification

Tested on Unraid 7.3.2, PHP 8.4.23, against the real Docker socket and the real registry, with the update-status cache redirected to a scratch file.

End to end, with no mocking at all: tag a throwaway alpine:3.19 to an older image, run the check, then update the image out of band with docker pull and run the check again, which is exactly what "Check for Updates" does.

======== master ========
  [1] first check while genuinely outdated
    local=de0eb0b3f2a4  remote=6baf43584bcb  status=false  ->  UPDATE AVAILABLE
  [2] out-of-band 'docker pull alpine:3.19'
  [3] user clicks 'Check for Updates'
    local=de0eb0b3f2a4  remote=6baf43584bcb  status=false  ->  UPDATE AVAILABLE   (stuck)

======== patched ========
  [1] first check while genuinely outdated
    local=de0eb0b3f2a4  remote=6baf43584bcb  status=false  ->  UPDATE AVAILABLE
  [2] out-of-band 'docker pull alpine:3.19'
  [3] user clicks 'Check for Updates'
    local=6baf43584bcb  remote=6baf43584bcb  status=true   ->  up-to-date

A case matrix driven through reloadUpdateStatus() with the registry digest as a controlled input passes on the patched code and fails on master for the out-of-band cases:

case expected master patched
out-of-band update, cache stale up-to-date update available up-to-date
out-of-band rollback, cache too new update available up-to-date update available
cold cache, image up to date up-to-date up-to-date up-to-date
genuine update available update available update available update available
already in sync, cache correct up-to-date up-to-date up-to-date
tag resolves to a non-last digest up-to-date update available up-to-date
no repo digest, cached value kept up-to-date up-to-date up-to-date

To check for false positives I compared every image on the server, 53 in total, cached digest against live inspect: 48 identical, so the patch changes nothing for them, 3 with a stale cache entry, and 2 without a repo digest that correctly keep their cached value. Resolving those against the real registry, master and the patched code report the same status for all of them, including redis:latest, which stays up-to-date only because of the digest preference described above.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Docker image update checks by refreshing local image information on each reload.
    • Improved digest matching between local images and remote registries.
    • Preserved the previously detected local version when a repository digest is unavailable.
    • More reliably identifies whether a local image matches the latest remote version.

…ge update

reloadUpdateStatus() took the local digest from the on-disk cache and only
inspected the image when that entry was missing, so once an image had been
checked its cached digest was trusted forever. Replacing an image outside the
Docker tab (docker pull, or a recreate through the API/CLI) never refreshed it,
so the tab stayed on "update available" and the update check kept notifying.
Check for Updates could not clear it either, since it runs the same code.

Inspect the local image on every check and fall back to the cached digest only
for images that carry no repo digest, such as locally built ones.

Making the live inspect authoritative also exposed which digest
inspectLocalVersion() picks. An image can carry several digests for the same
repo, because a multiarch tag that was re-pushed keeps the older manifest list
as well, and the tag does not always resolve to the last entry. redis:latest
currently resolves to RepoDigests[0], so always taking the last one reported a
spurious update. Prefer the digest the tag resolves to when the image carries
it, and keep the last entry as the fallback.

Fixes unraid#2710
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ec018a2-a18d-49e1-861a-8dab61ad6ea4

📥 Commits

Reviewing files that changed from the base of the PR and between d24a9c0 and 5b60d0b.

📒 Files selected for processing (1)
  • emhttp/plugins/dynamix.docker.manager/include/DockerClient.php

Walkthrough

Docker update checks now retrieve the remote digest first, re-inspect local images on every reload, preserve cached values only when inspection returns null, and match remote digests against all valid local repository digests.

Changes

Docker update status

Layer / File(s) Summary
Digest refresh and matching
emhttp/plugins/dynamix.docker.manager/include/DockerClient.php
reloadUpdateStatus retrieves the remote version before local inspection and stores the refreshed values. inspectLocalVersion accepts the remote digest, checks all valid SHA-256 repository digests, and returns the matching or final digest.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Mergeability Score: ⚪ Minimal · up to 5b60d

The change refreshes Docker image digests so out-of-band updates correctly clear stale update notices; no actionable merge-blocking risk remains beyond normal checks and review.

Poem

I’m a rabbit with a digest to check,
Fresh image clues arrive on deck.
I sniff each hash in the row,
Match the remote one when they show.
The update flag now knows where to go.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes clearing stale Docker update status after an out-of-band image update.
Linked Issues check ✅ Passed The changes re-inspect local images and recompute status, directly addressing issue #2710 and supporting digest fallback cases.
Out of Scope Changes check ✅ Passed All changes are confined to Docker digest inspection and update-status recalculation required by issue #2710.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Docker tab keeps showing "update available" after a container is updated via the Docker API

1 participant