Find exact tag of Docker image
I had a number of old docker images stored locally and didn't have all the tag info. I think I used ":latest" on most of them, but apparently calling an image that does not mean that every time the container restarts (at host reboot), it fetches the latest one. So some of my images were woefully behind, and as part of a good practice to upgrade to current images for everything, I wanted to know exactly which image I had before, in case I need to roll back.
And apparently there's not a good process for determining that. So I had to write a script to do it for me. And then I had to hack it to work with github container registry which operates differently than docker hub, in this case not as efficiently as docker hub.
We'll start with the script and then I'll explain it.
files/2025/listings/find-docker-image-tags.sh (Source)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
#!/bin/sh # File: /mnt/public/Support/Programs/docker/find-docker-image-tags.sh # Aliases: vm4:~/bin/find-docker-image-tags.sh # Startdate: 2025-04-09-4 14:17 # Purpose: given the sha256:<tag> of an image that I have downloaded from `docker images --digests`, find it in docker hub so I can get the exact tag name. I tend to use "<imagename>:latest" but that does not capture the tag info about it. # Usage: # Search: # docker image uses latest, how can i determine tag number # docker find which release based on sha256 tag # curl vnd.oci.image.index.v1+json tags list start # Reference: # https://gist.github.com/achetronic/2db363e6c2fbecd42ae67512fbea50ca # https://stackoverflow.com/questions/49632521/how-to-add-a-field-to-a-json-object-with-the-jq-command # https://toddysm.com/2024/02/12/authenticating-with-oci-registries-github-container-registry-ghcr-implementation/ # https://serverfault.com/questions/1164750/manifest-unknown-error-oci-index-found-but-accept-header-does-not-support-oci # https://docker-docs.uclv.cu/registry/spec/manifest-v2-2/ but should have been https://docs.docker.com/registry/spec/manifest-v2-2/ as linked from https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry # https://unix.stackexchange.com/questions/776571/how-to-use-jq-to-get-a-value-from-two-possible-paths # https://tech.michaelaltfield.net/2024/09/03/container-download-curl-wget/ # Improve: # Finding the tag for IMAGENAME based on a docker hub one. This is the standard, "asdf/asdf" when there is not a dot in the first part of the image name. dockerhub() { # Needs: IMAGENAME # head: only grab first one digest="$( docker images --digests "${IMAGENAME}" --format json | head -n1 | jq --raw-output '.Digest' )" # resembles sha256:c07f5319d20bdbd58a19d7d779a1e97159ce25cb95572baa947c70f58589937c # only check first 50 pages echo "Looking for digest ${digest}" 1>&2 for i in $( seq 1 50 ) ; do path="https://registry.hub.docker.com/v2/repositories/${IMAGENAME}/tags" echo "${path}?page=${i}" 1>&2 output="$( curl --silent "${path}?page=${i}" \ | jq --compact-output "[.results[] | select(.[\"images\"][][\"digest\"] == \"${digest}\" or .digest == \"${digest}\") | {name,digest}] | unique | .[]" )" if test -n "${output}" ; then echo "${output}" | jq ". += {\"page\":\"${i}\",\"image\":\"${IMAGENAME}\"}" return 0 fi done } # Find it from github container registry, including the proxy-name lscr. ghcrio() { digest="$( docker images --digests "${IMAGENAME}" --format json | head -n1 | jq --raw-output '.Digest' )" echo "Looking for digest ${digest}" 1>&2 # authenticate to github using my token # any old repository should do, unless it does not and you have to re-auth to get the scope, even though the token value is exactly the same. # WORKHERE: visit ghcr.io/minetest-mapserver/mapserver and follow all redirects and derive scope=repository:<HERE> token="$( curl --silent --header 'Accept: application/json' \ --header "Authorization: Basic $( printf '%s' "username:plaintexttokenhere" | base64 )" \ -L 'https://ghcr.io/token?service=ghcr.io&scope=repository:minetest-mapserver/mapserver:pull' \ | jq --raw-output '.token' )" #-L 'https://ghcr.io/token?service=ghcr.io&scope=repository:linuxserver/docker-luanti:pull' echo "Using token ${token}" 1>&2 # strip leading hostname part of IMAGENAME _imagename="$( echo "${IMAGENAME}" | sed -r -e 's@^[^/]+\/@@;' )" # flow: look up the exact sha256:<digest>, and get its .manifests[0].digest and then compare to all tags. _new_digest="$( curl --silent -L --header "Authorization: Bearer ${token}" --header "Accept: application/vnd.oci.image.index.v1+json" "https://ghcr.io/v2/${_imagename}/manifests/${digest}" | jq --raw-output '.manifests[0].digest // .config.digest' )" echo "Looking for _new_digest ${_new_digest}" 1>&2 # get all tags # n=500 gets the long tail on most things tags="$( curl --silent -L --header "Authorization: Bearer ${token}" "https://ghcr.io/v2/${_imagename}/tags/list?n=500" | jq --raw-output '.tags[]' )" echo "Tag count: $( echo "${tags}" | wc -l )" 1>&2 good_tags="" for tag in ${tags} ; do printf "Checking tag ${tag}: " 1>&2 manifest="$( curl --silent -L --header "Authorization: Bearer ${token}" --header "Accept: application/vnd.oci.image.index.v1+json" "https://ghcr.io/v2/${_imagename}/manifests/${tag}" )" echo "${manifest}" 1>&2 if echo "${manifest}" | jq -e ".manifests[] | select(.[\"digest\"] == \"${_new_digest}\")" 1>/dev/null 2>/dev/null || echo "${manifest}" | jq -e ".config.digest == \"${_new_digest}\"" 1>/dev/null 2>/dev/null then #echo "Tag for SHA256 ${digest}, discovered digest ${_new_digest} is ${tag}" good_tags="${good_tags:+${good_tags},}${tag}" #break fi done if test -n "${good_tags}" ; then echo "Image ${IMAGENAME} is on tags: ${good_tags}" fi } main() { IMAGENAME="${IMAGENAME:-${1}}" test -z "${IMAGENAME}" && { echo "Fatal! Need IMAGENAME or \$1. Aborted." 1>&2 ; exit 1 ; } _has_dots="$( echo "${IMAGENAME}" | awk -F'/' '$1~/\./' )" if test -n "${_has_dots}" ; then ghcrio else # assume default of dockerhub dockerhub fi } main "${@}" |
Docker Hub lets you search for all the tags which also displays digest, so you don't have as many requests to make. If the loop finds the digest that matches, it will print the tags in a json format.
For Github container registry, you would need to prepare your username:plaintexttoken first. The script has to make many requests, because the list of tags does not include any info about each one. It has to go query each one, so it takes longer. Also, the token scope might be limited. I haven't sorted out the scope=repository automatically. I just re-ran it with the exact scope. Some projects use a different repo name than the image name, so it's not just about dropping the image name in there. For example, visiting https://github.com/linuxserver/docker-luanti/pkgs/container/luanti shows you the installation command:
docker pull ghcr.io/linuxserver/luanti:5.11.0-ls10
So it appears to be the github repo name as the scope, which is outside knowledge for docker cli purposes. So I haven't solved that yet.
References
- Find the tag of a Docker image having only the SHA256
- How to add a field to a JSON object with the jq command? - Stack Overflow
- Authenticating with OCI Registries - GitHub Container Registry (GHCR) Implementation - ToddySM
- docker - MANIFEST_UNKNOWN error: OCI index found, but Accept header does not support OCI indexes - Server Fault
- Image Manifest V 2, Schema 2 | Docker Documentation but should have been https://docs.docker.com/registry/spec/manifest-v2-2/ as linked from Working with the Container registry - GitHub Docs
- json - How to use jq to get a value from two possible paths? - Unix & Linux Stack Exchange
- Manually Downloading Container Images (Docker, Github Packages) - Michael Altfield's Tech Blog
- Need to answer this: dockerhub - Finding the actual version of latest version of docker image - Stack Overflow
Comments