Skip to content

Instantly share code, notes, and snippets.

@jaytaylor
Last active May 10, 2024 16:28
Show Gist options
  • Save jaytaylor/86d5efaddda926a25fa68c263830dac1 to your computer and use it in GitHub Desktop.
Save jaytaylor/86d5efaddda926a25fa68c263830dac1 to your computer and use it in GitHub Desktop.
One liner for deleting images from a v2 docker registry

One liner for deleting images from a v2 docker registry

Just plug in your own values for registry and repo/image name.

registry='localhost:5000'
name='my-image'
curl -v -sSL -X DELETE "http://${registry}/v2/${name}/manifests/$(
    curl -sSL -I \
        -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
        "http://${registry}/v2/${name}/manifests/$(
            curl -sSL "http://${registry}/v2/${name}/tags/list" | jq -r '.tags[0]'
        )" \
    | awk '$1 == "Docker-Content-Digest:" { print $2 }' \
    | tr -d $'\r' \
)"

If all goes well

* About to connect() to localhost port 5000 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
> DELETE /v2/my-image/manifests/sha256:14f6ecba1981e49eb4552d1a29881bc315d5160c6547fdd100948a9e30a90dff HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:5000
> Accept: */*
>
< HTTP/1.1 202 Accepted
< Docker-Distribution-Api-Version: registry/2.0
< X-Content-Type-Options: nosniff
< Date: Wed, 15 Nov 2017 23:25:30 GMT
< Content-Length: 0
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact

Garbage cleanup

Finally, invoke garbage cleanup on the docker-registry container.

For example:

docker exec -it docker-registry bin/registry garbage-collect /etc/docker/registry/config.yml
@oerp-odoo
Copy link

Does anyone know if this is possible for Official Docker Cloud? Is there a way to enable this option too?

Im getting b'{"message":"405: Method Not Allowed"}\n'. Which is ironic, that they do not even mention that in their API docs and you might think it would work by default, but it is not.

As its documented here: https://docs.docker.com/docker-hub/api/latest/#operation/PostNamespacesDeleteImages
They do not even mention that 405 error code can be returned.

@roozbehk
Copy link

i have updated the script and used it at the moment.

#!/bin/bash

# based on: https://gist.github.com/jaytaylor/86d5efaddda926a25fa68c263830dac1
# changes: 
# - iterate over tag list instead of using only first one
# - added basic auth
# - fixed http header to 'docker-content-digest'


# exit when any command fails
set -e

registry='localhost:5000'

# concants all images listed in json file into single line string seperated with blank
images="image image2..."

echo "Registry User:"
read user
echo "Registry Password:"
read -s password

for image in $images; do
    echo "DELETING: " $image

    # get tag list of image, with fallback to empty array when value is null
    tags=$(curl --user $user:$password "https://${registry}/v2/${image}/tags/list" | jq -r '.tags // [] | .[]' | tr '\n' ' ')

    # check for empty tag list, e.g. when already cleaned up
    if [[ -n $tags ]]
    then
        for tag in $tags; do
            curl --user $user:$password -X DELETE "https://${registry}/v2/${image}/manifests/$(
                curl --user $user:$password -I \
                    -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
                    "https://${registry}/v2/${image}/manifests/${tag}" \
                | awk '$1 == "docker-content-digest:" { print $2 }' \
                | tr -d $'\r' \
            )"
        done

        echo "DONE:" $image
    else
        echo "SKIP:" $image
    fi
done

FYI, I had an issue with your script and I ended up debugging it. Curl was expecting the header parameter 'Docker-Content-Digest' to be capitalized.

@locvfx
Copy link

locvfx commented Oct 31, 2022

THANK YOU @kirilkirkov
I confirm the solution works.
Finally I got a working script to delete private docker image

@PankajN18
Copy link

Execute following on the DockerRegistry server:

registry='registery-server-ip:port'
repo_name='repo_name_here'
curl -v -sSL -X DELETE "http://${registry}/v2/${repo_name}/manifests/$(
curl -sSL -I
-H "Accept: application/vnd.docker.distribution.manifest.v2+json"
"http://${registry}/v2/${repo_name}/manifests/$(
curl -sSL "http://${registry}/v2/${repo_name}/tags/list" | jq -r '.tags[0]'
)"
| awk '$1 == "Docker-Content-Digest:" { print $2 }'
| tr -d $'\r'
)"

Delete the repository DIR in registry-container

docker exec -it registry-container-name rm -rf /var/lib/registry/docker/registry/v2/repositories/${repo_name}

Restart the RegistryContainer:

docker restart registry-container-name

Now, you should be able to push the same name image.

-- PankajS

@erichorwath
Copy link

erichorwath commented Mar 27, 2023

on registry 2.7.1, if you see this error:
delete OCI index found, but accept header does not support OCI indexes

Then following header might help:

curl -v -sSL -X DELETE "http://${registry}/v2/${name}/manifests/$(
    curl -sSL -I \
        -H "Accept: application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json" -X GET  \
        "http://${registry}/v2/${name}/manifests/$(
            curl -sSL "http://${registry}/v2/${name}/tags/list" | jq -r '.tags[0]'
        )" \
    | awk '$1 == "docker-content-digest:" { print $2 }' \
    | tr -d $'\r' \
)"

And be careful with the docker-content-digest: spelling and https/http.

@DKroot
Copy link

DKroot commented Aug 17, 2023

Based on all this discussion, I put together a complete script for deleting a single image: all tags or specified tags. Tested with the latest Registry (2.8.2).

docker-rmi-registry.sh:

#!/usr/bin/env bash
# Credits (based on): J. Elliot Taylor https://gist.github.com/jaytaylor/86d5efaddda926a25fa68c263830dac1
# Changes:
# - iterate over tag list instead of using only first one
# - optional: basic auth

set -e
set -o pipefail

readonly VER=1.0.0
REGISTRY_URL='http://localhost:5000'
REGISTRY_CONTAINER=registry

# Remove the longest `*/` prefix
readonly SCRIPT_FULL_NAME="${0##*/}"

usage() {
  cat <<HEREDOC
NAME

    $SCRIPT_FULL_NAME -- remove the image(s) from the local Docker Registry

SYNOPSIS

    $SCRIPT_FULL_NAME [-r registry_url] [-c container] [-u user:password] image_name [tag 1] [tag 2] ...
    $SCRIPT_FULL_NAME -h: display this help

DESCRIPTION

    Removes the tagged image(s) from the local Docker Registry.

    The following options are available:

    image_name  image name in the Registry
    tag1 ...    (optional) remove specific tagged image. If not specified, remove **all** tagged images.

    -r registry_url (optional) Registry URL, defaults to $REGISTRY_URL
    -c container    (optional) Registry container name or id, defaults to \`$REGISTRY_CONTAINER\`
    -u              (optional) Registry credentials

ENVIRONMENT

    * Docker Registry container running locally.

EXAMPLES

        $ $SCRIPT_FULL_NAME my_app 1.4.1

v$VER
HEREDOC
  exit 1
}

# If a character is followed by a colon, the option is expected to have an argument
while getopts r:c:u:h OPT; do
  case "$OPT" in
    r)
      readonly REGISTRY_URL="$OPTARG"
      ;;
    c)
      readonly REGISTRY_CONTAINER=$OPTARG
      ;;
    u)
      readonly CRED_OPT=(--user "$OPTARG")
      ;;
    *) # -h or `?`: an unknown option
      usage
      ;;
  esac
done
echo -e "\n[$(date +'%T %Z') v$VER] ${USER:-${USERNAME:-${LOGNAME:-UID #$UID}}}@${HOSTNAME} ${PWD}> $0${*+ }$*\n"
shift $((OPTIND - 1))

# Process positional parameters
readonly IMAGE=$1
if [[ ! $IMAGE ]]; then
  usage
fi
shift

while (($# > 0)); do
  tags+=("$1")
  shift
done

if ((${#tags[@]} == 0)); then
  # Get tag list
  tag_list=$(curl "${CRED_OPT[@]}" --silent --show-error "${REGISTRY_URL}/v2/${IMAGE}/tags/list" | jq -r '.tags[]?')
  readarray -t tags <<<"$tag_list"
fi

# check for empty tag list, e.g. when already cleaned up
if ((${#tags[@]} == 0)); then
  echo "No $IMAGE images found"
  exit
fi

deleted=false
for tag in "${tags[@]}"; do
  image_digest=$(curl --head --header "Accept: application/vnd.docker.distribution.manifest.v2+json" "${CRED_OPT[@]}" \
      --silent --show-error "${REGISTRY_URL}/v2/${IMAGE}/manifests/${tag}" \
    | awk '$1 == "Docker-Content-Digest:" { print $2 }' \
    | tr -d $'\r'
  )
  if [[ $image_digest ]]; then
    echo "DELETING $IMAGE:$tag"
    curl --request DELETE "${CRED_OPT[@]}" --silent --show-error "${REGISTRY_URL}/v2/${IMAGE}/manifests/${image_digest}"
    deleted=true
    echo "DELETED $IMAGE:$tag"
  else
    echo "No $IMAGE:$tag image found"
  fi
done

if [[ $deleted == true ]]; then
  echo -e "\nCleaning up after deletion\n"
  # -m delete manifests that are not currently referenced via tag
  docker exec -it "$REGISTRY_CONTAINER" bin/registry garbage-collect /etc/docker/registry/config.yml -m

  echo -e "\nRestarting Registry"
  docker restart "$REGISTRY_CONTAINER"
fi

@di-rect
Copy link

di-rect commented Sep 14, 2023

@DKroot Looks good, thanks!

Is it not an idea to select on max age or how many tags/versions of an image ?

@DKroot
Copy link

DKroot commented Sep 14, 2023

I just needed to remove specific versions of 1 image or all versions of 1 image.

@lautitoti
Copy link

@DKroot Looks good, thanks!

Is it not an idea to select on max age or how many tags/versions of an image ?

I needed the same @di-rect , so I hope it helps you (or someone else) too :)

#!/bin/bash

# based on: https://gist.github.com/jaytaylor/86d5efaddda926a25fa68c263830dac1
# changes: 
# - Check for images older than a year 
-----------
# Function to check tags older than a year
check_tags_older_than_a_year() {
    local registry=$1
    local image=$2
    local user=$3
    local password=$4

    local tags=$(curl -s -u "$user:$password" "https://${registry}/v2/${image}/tags/list" | jq -r '.tags // [] | .[]')

    if [[ -n $tags ]]; then
        for tag in $tags; do
            # For manifests v2 uncomment the following line and comment the v1 version.
            # local created_time=$(curl -s -I -u "$user:$password" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "https://${registry}/v2/${image}/manifests/${tag}" | jq -r '[.history[]]|map(.v1Compatibility|fromjson|.created)|sort|reverse|.[0]')
            local created_time=$(curl -s -u $user:$password -H 'Accept: application/vnd.docker.distribution.manifest.v1+json' -X GET https://$registry/v2/$image/manifests/$tag | jq -r '[.history[]]|map(.v1Compatibility|fromjson|.created)|sort|reverse|.[0]')
            local created_timestamp=$(date -d "$created_time" +%s)
            local current_timestamp=$(date +%s)
            local age_days=$(( (current_timestamp - created_timestamp) / (60*60*24) ))

            if [[ $age_days -gt 365 ]]; then
                echo "Tag $tag of image $image is older than a year. Deleting..."
                # Uncomment the following line to actually delete the tag
                curl -s -u "$user:$password" -X DELETE "https://${registry}/v2/${image}/manifests/$(curl -s -I -u "$user:$password" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "https://${registry}/v2/${image}/manifests/${tag}" | grep -i 'docker-content-digest' | awk -F': ' '{print $2}' | sed 's/[\r\n]//g')"
            fi
        done
    else
        echo "No tags found for image $image."
    fi
}

# Main function
main() {
    echo "Specify private image registry url without https://"
    read -r registry

    echo "List images, separated by space"
    read -r images

    echo "Registry User:"
    read -r user

    echo "Registry Password:"
    read -s password

    IFS=' ' read -r -a images_array <<< "$images"
    for image in "${images_array[@]}"; do
        echo "Checking tags for image $image..."
        check_tags_older_than_a_year "$registry" "$image" "$user" "$password"
    done
}

# Run the main function
main

@ehdis
Copy link

ehdis commented Mar 1, 2024

One liner :-)
skopeo delete docker://localhost:5000/apps/registry:latest

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