One of the great things about Gitea, is that it comes with a built-in Docker registry. This means that you don’t need to be reliant on Docker Hub (or its rate-limits) to host your docker images, or any other OCI compliant image for that matter.

In this post, I’ll go over how I migrated some of my docker images from the Docker Hub to my own self-hosted registry in Gitea. I won’t go over the setup of the package registry in Gitea itself, as it’s enabled by default, and I’d rather focus on the migration of images itself. The approach I took also didn’t use any registry specific tools, and so it can be generalized to most other registries.

The MVP of this whole thing is a tool called skopeo. It is a command line tool that allows for many different operations on container images, and image registries. It also doesn’t need to be run as root, or have an active docker daemon running.

While skopeo is a great tool, it can’t fetch the entire list of images in a certain namespace, so I had to use some bash scripting to fetch the list of images, loop over them, and pass each of them to skopeo. Some caveats to be aware of, this script uses skopeo sync instead of skopeo copy, and that means that sync may remove tags from the destination if the image already exists. The reason for choosing sync, rather than copy, is that it can handle all of the tags at once, instead of having to do another API to fetch the list of tags.

#!/bin/bash

## Variables to change
# TODO: these shouldn't be hardcoded, but rather passed in as arguments or via env vars
#       but that's a future problem

# Docker Hub credentials
HUB_NAMESPACE="<namespace>" # the user/org namespace on Docker Hub that you want to bring over
HUB_USERNAME="<username>"
HUB_PASSWORD="<password>"

GITEA_DOMAIN="<domain>" # e.g. gitea.example.com
GITEA_PACKAGE_NAMESPACE="<namespace>" # the user/org that you wish to store the images under
GITEA_USERNAME="<username>" # user that has access to the package registry namespace
GITEA_TOKEN="<password>" # token needs the packages:write permission

# Function to check if a command exists
command_exists() {
    command -v "$1" >/dev/null 2>&1
}

# Check if jq and skopeo are installed
if ! command_exists jq || ! command_exists skopeo; then
    echo "either jq or skopeo is missing, please make sure they are both installed" >&2
    exit 1
fi

# login into Docker Hub and fetch an API token
# its possible to do this without the token, but you'll likely run into ratelimits, and it would also only provide public images
TOKEN=$(curl -s -H "Content-Type: application/json" -X POST -d '{"username": "'${HUB_USERNAME}'", "password": "'${HUB_PASSWORD}'"}' https://hub.docker.com/v2/users/login/ | jq -r .token)

# Get list 100 of images that the namespace has, if you have more than that then pagination will need to be added
REPOS=$(curl -s -H "Authorization: JWT ${TOKEN}" "https://hub.docker.com/v2/repositories/${HUB_NAMESPACE}/?page_size=100" | jq -r '.results[].name')
# TODO: The fetching of images above is Docker Hub API specific, and so if your source is non-Docker Hub this will need to be adjusted

# Loop through images and pass them to skopeo
for repo in $REPOS; do
    echo "Syncing ${HUB_NAMESPACE}/${repo} to ${GITEA_DOMAIN}/${GITEA_PACKAGE_NAMESPACE}/${repo}"
    skopeo sync \
        --src docker --src-creds ${HUB_USERNAME}:${HUB_PASSWORD} \
        --dest docker --dest-creds ${GITEA_USERNAME}:${GITEA_TOKEN} \
        "docker://docker.io/${HUB_NAMESPACE}/${repo}" \
        "docker://${GITEA_DOMAIN}/${GITEA_NAMESPACE}/${repo}"
done

echo "Finished moving images!"

You can also use this script to copy images from other namespaces, so if you want to have a copy of another namespace, say for example Bitnami, you could use this script to do that as well.

This script has helped me move over a lot of images, and it’s been working great for me. It’s missing a few things, like passing in the options as arguments, error handling, or pagination for the Docker Hub API, but I didn’t have more than 100 images in my namespace, so I didn’t need to worry about that. If you do use this script, and modify it, I’d love to hear about it.

If you want to read more about the skopeo sync options, you can check out the skopeo sync documentation.

Disclaimer: I am a maintainer of Gitea. You can use this with other registries as well, so you don’t need to use Gitea to use this script.