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
@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