Skip to content

Instantly share code, notes, and snippets.

@jaytaylor
Last active April 19, 2024 18:12
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
@IoTPlay
Copy link

IoTPlay commented Jan 28, 2018

thanks ! I get an error, do you have an idea where I should look?

*   Trying fe80::1ee9:8168:e5f7:5ad...
* TCP_NODELAY set
* Connected to rh01.local (fe80::1ee9:8168:e5f7:5ad) port 5000 (#0)
> DELETE /v2/alpine-mariadb/manifests/sha256:bab2e6d660df03fd1d6f5285f7026755e34c25f32d1c7f5a3e2688bbc038c458 HTTP/1.1
> Host: rh01.local:5000
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 405 Method Not Allowed
< Content-Type: application/json; charset=utf-8
< Docker-Distribution-Api-Version: registry/2.0
< X-Content-Type-Options: nosniff
< Date: Sun, 28 Jan 2018 10:48:21 GMT
< Content-Length: 78
< 
{"errors":[{"code":"UNSUPPORTED","message":"The operation is unsupported."}]}
* Connection #0 to host rh01.local left intact

@MoLow
Copy link

MoLow commented May 30, 2018

You should set the environment variable REGISTRY_STORAGE_DELETE_ENABLED=true

@vanbroup
Copy link

A small update for when using https and basic auth:

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

@ykyuen
Copy link

ykyuen commented Jul 30, 2018

thanks for the snippet, i made a small change in the regular expression such that it could get the "Docker-Content-Digest" case-insensitively.

moreover, you could specify the tag which u want to delete. here you go.

registry='<registry host>'
name='<image name>'
auth='-u <username>:<password>'
tag='<tag>'
curl $auth -X DELETE -sI -k "https://${registry}/v2/${name}/manifests/$(
  curl $auth -sI -k \
    -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
    "https://${registry}/v2/${name}/manifests/${tag}" \
    | tr -d '\r' | sed -En 's/^Docker-Content-Digest: (.*)/\1/pi'
)"

@petershaw
Copy link

I am on registry 2.6.2 and I have enabled delete in the config.yml.
The delete call give me a 405. Do someone have a hint?

HTTP/1.1 405 Method Not Allowed
Server: nginx/1.13.3
Date: Tue, 28 Aug 2018 17:04:20 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 78
Connection: keep-alive
Docker-Distribution-Api-Version: registry/2.0
X-Content-Type-Options: nosniff

@PotatoAdmin
Copy link

Restarting the repository after altering the config.yml did the trick for me.

@JimiC
Copy link

JimiC commented Jun 4, 2019

You can also start the registry with DELETE enabled without messing with the config.yml.

i.e. docker run -d -p 5000:5000 --restart=always --name registry -v /certs:/certs -e REGISTRY_STORAGE_DELETE_ENABLED=true -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key registry:2

Note: Adjust TLS flags accordingly to your needs.

@JimiC
Copy link

JimiC commented Jun 4, 2019

Garbage cleanup worked for me by adding the -m flag. i.e. docker exec -it docker-registry bin/registry garbage-collect /etc/docker/registry/config.yml -m

@Hoangnse03193
Copy link

For
i tried above script, all goes well until after i run Garbage cleanup.
i try to push the image that i deleted to local registry again i got msg
" Layer already exists".
After that when i try to pull this image by docker and i got error
"Error response from daemon: manifest for localhost:5000/my-ubuntu:latest not found: manifest unknown: manifest unknown"

Any one know what happen and how to fix it?

@rodjjo
Copy link

rodjjo commented Jul 5, 2019

why not docker image remove localhost:5000/my-image-name:latest

(edited)
oh sorry, just deletes local tag :(

@Kevinrob
Copy link

Based on this, I made a version that delete all tags for all repositories with microk8s => https://gist.github.com/Kevinrob/4c7f1e5dbf6ce4e94d6ba2bfeff37aeb

@Silentnight93
Copy link

any repository can be deleted by accessing the container's shell
"docker exec -ti --privileged [repository name] bin/sh"
after that access "/var/lib/registry/docker/registry/v2/repositories/" and delete the folder with the repository name youwant to delete
anf if u want to delete a repository tag "_manifests/tags/" and the tag associated with the version you want to delete

@lalith-b
Copy link

lalith-b commented Apr 9, 2020

just find the volume of the local repository and delete the folder ? it should solve it.

@wleenavo
Copy link

wleenavo commented Apr 29, 2020

removed folder /var/lib/registry/docker/registry/v2/repositories/[name]/_manifests/tags/[associatedTags] and then execute

docker exec -it registry bin/registry garbage-collect /etc/docker/registry/config.yml -m

@moisei
Copy link

moisei commented Nov 18, 2020

I've used this one to delete all the tags in the multiple repos.
Just put the repos list to the repos-todel file.
Run curl -sSL "http://${registry}/v2/_catalog?n=1000" | jq .repositories[] to extract full list of the repos.

https://gist.github.com/moisei/ef182a8c32b1bdd29139ad5c7f9ad2f0#file-cleanup-docker-registry

@tkakantousis
Copy link

For
i tried above script, all goes well until after i run Garbage cleanup.
i try to push the image that i deleted to local registry again i got msg
" Layer already exists".
After that when i try to pull this image by docker and i got error
"Error response from daemon: manifest for localhost:5000/my-ubuntu:latest not found: manifest unknown: manifest unknown"

Any one know what happen and how to fix it?

Hi @Hoangnse03193

I am facing the same issue, did you manage to find a solution?

@deleteriousEffect
Copy link

" Layer already exists".

This might be due to the cache. The garbage collector doesn't communicate with the cache, or unlink layers from the repository so if you immediately try to repush a layer that was just deleted, the registry will find it for stat calls, but actually serving the blob will fail.

@wobu
Copy link

wobu commented Mar 31, 2021

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

@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