This is a bit more complex than it seems on the surface if you want to delete a tag and not an image manifest. First a prerequisite, your registry needs to permit deleting images which isn't turned on by default in the registry:2
image. The easy way to enable that is setting the environment variable REGISTRY_STORAGE_DELETE_ENABLED=true
on the container.
Next, realize the difference between a tag and an image manifest. The manifest is represented by a digest, and is the json data that points to the image config and layers. A tag points to a manifest, but multiple tags may point to the same manifest, and a manifest may not have any tags pointing to it. If you delete a manifest, that also removes all tags that point to the manifest, so you need to be careful when deleting tags that you don't accidentally delete a manifest referenced by tags you want to keep.
Therefore, the normal way to do this has issues. That normal way is to query the registry for the digest of the manifest you want to delete, and then delete that digest. You can get that digest from the headers:
acceptM="application/vnd.docker.distribution.manifest.v2+json"
acceptML="application/vnd.docker.distribution.manifest.list.v2+json"
curl -H "Accept: ${acceptM}" \
-H "Accept: ${acceptML}" \
-I -s "https://registry.example.org/v2/${repo}/manifests/${tag}"
And then a delete request on that digest would delete the manifest, along with all tags that also point to it:
curl -H "Accept: ${acceptM}" \
-H "Accept: ${acceptML}" \
-X DELETE -s "https://registry.example.org/v2/${repo}/manifests/${digest}"
However, if you want to delete just the tag, there is a delete tag API in distribution-spec, but few registries have implemented it. That delete would look like:
curl -H "Accept: ${acceptM}" \
-H "Accept: ${acceptML}" \
-X DELETE -s "https://registry.example.org/v2/${repo}/manifests/${tag}"
For registries that don't support this, the best solution I've found is to push a dummy manifest that replaces the tag, and then delete that dummy manifest. That gets to be a bit much to handle with curl, there's more media type headers I haven't been including, and this doesn't talk about authentication. For all of those challenges, I turn to writing it in Go. My own tool for that is regclient, and others like skopeo and crane also exist. From regclient, the regctl command to do this looks like:
regctl tag rm registry.example.org/image1:T12
regctl tag rm registry.example.org/image2:T22
regctl tag rm registry.example.org/image5:T22
Once the image is deleted, you likely want to clean the storage that is used, and for that you need to run a garbage collection while no other pushes are in progress (some will disable the registry or wait until a time they know uploads will not run). With the registry:2
image, a GC command looks like:
docker exec registry /bin/registry garbage-collect \
/etc/docker/registry/config.yml --delete-untagged
which will delete all untagged manifests, along with any unreferenced blobs.
Caution: untagged manifests in the distribution registry currently includes all child manifests of a multi-platform image. This means deleting untagged manifests will likely lead to data-loss if you have multi-platform images in your registry. There's issue 3178 to track when this will be resolved.