Bulk migrate images
This guide shows you how to migrate Docker images in bulk between Docker Hub organizations or namespaces. Whether you're consolidating repositories, changing organization structure, or moving images to a new account, these techniques help you migrate efficiently while preserving image integrity.
The topic is structured to build up in scale:
The recommended tool for this workflow is crane. An equivalent alternative
using regctl is also shown. Both tools perform registry-to-registry copies
without pulling images locally and preserve multi-architecture images.
crane is recommended for its simplicity and focused image-copying workflow.
regctl is also a good choice, particularly if you already use it for broader
registry management tasks beyond image copying.
NoteThe main workflows in this topic operate on tagged images only. Untagged manifests or content no longer reachable from tags are not migrated. In practice, these are usually unused artifacts, but be aware of this limitation before migration. While you can migrate specific untagged manifests using digest references, there is no API to enumerate untagged manifests in a repository.
Prerequisites
Before you begin, ensure you have:
- One of the following installed and available in your
$PATH: - Push access to both the source and destination organizations
- Registry authentication configured for your chosen tool
Authenticate to registries
Both tools authenticate directly against registries:
craneuses Docker credential helpers and~/.docker/config.json. See the crane documentation.regctluses its own configuration file and can import Docker credentials. See the regctl documentation.
Follow the authentication instructions for your registry and tool of choice.
Migrate a single image tag
This is the simplest and most common migration scenario.
The following example script copies the image manifest directly between registries and preserves multi-architecture images when present. Repeat this process for each tag you want to migrate. Replace the environment variable values with your source and destination organization names, repository name, and tag.
#!/usr/bin/env bash
set -euo pipefail
SRC_ORG="oldorg"
DEST_ORG="neworg"
REPO="myapp"
TAG="1.2.3"
SRC_IMAGE="${SRC_ORG}/${REPO}:${TAG}"
DEST_IMAGE="${DEST_ORG}/${REPO}:${TAG}"
# Using crane (recommended)
crane cp "${SRC_IMAGE}" "${DEST_IMAGE}"
# Using regctl (alternative)
# regctl image copy "${SRC_IMAGE}" "${DEST_IMAGE}"Migrate by digest
To migrate a specific image by digest instead of tag, use the digest in the
source reference. This is useful when you need to migrate an exact image
version, even if the tag has been updated. Replace the environment variable
values with your source and destination organization names, repository name,
digest, and tag. You can choose between crane and regctl for the copy
operation.
#!/usr/bin/env bash
set -euo pipefail
SRC_ORG="oldorg"
DEST_ORG="neworg"
REPO="myapp"
DIGEST="sha256:abcd1234..."
TAG="stable"
SRC_IMAGE="${SRC_ORG}/${REPO}@${DIGEST}"
DEST_IMAGE="${DEST_ORG}/${REPO}:${TAG}"
# Using crane
crane cp "${SRC_IMAGE}" "${DEST_IMAGE}"
# Using regctl
# regctl image copy "${SRC_IMAGE}" "${DEST_IMAGE}"Migrate all tags for a repository
To migrate every tagged image in a repository, use the Docker Hub API to enumerate tags and copy each one. The following example script retrieves all tags for a given repository and migrates them in a loop. This approach scales to repositories with many tags without overwhelming local resources. Note that there is a rate limit on Docker Hub requests, so you may need to add delays or pagination handling for large repositories.
Replace the environment variable values with your source and destination
organization names and repository name. If your source repository is private,
also set HUB_USER and HUB_TOKEN with credentials that have pull access. You
can also choose between crane and regctl for the copy operation.
#!/usr/bin/env bash
set -euo pipefail
# Use environment variables if set, otherwise use defaults
SRC_ORG="${SRC_ORG:-oldorg}"
DEST_ORG="${DEST_ORG:-neworg}"
REPO="${REPO:-myapp}"
# Optional: for private repositories
# HUB_USER="your-username"
# HUB_TOKEN="your-access-token"
# AUTH="-u ${HUB_USER}:${HUB_TOKEN}"
AUTH=""
TOOL="crane" # or: TOOL="regctl"
TAGS_URL="https://hub.docker.com/v2/repositories/${SRC_ORG}/${REPO}/tags?page_size=100"
while [[ -n "${TAGS_URL}" && "${TAGS_URL}" != "null" ]]; do
RESP=$(curl -fsSL ${AUTH} "${TAGS_URL}")
echo "${RESP}" | jq -r '.results[].name' | while read -r TAG; do
[[ -z "${TAG}" ]] && continue
SRC_IMAGE="${SRC_ORG}/${REPO}:${TAG}"
DEST_IMAGE="${DEST_ORG}/${REPO}:${TAG}"
echo "Migrating ${SRC_IMAGE} → ${DEST_IMAGE}"
case "${TOOL}" in
crane)
crane cp "${SRC_IMAGE}" "${DEST_IMAGE}"
;;
regctl)
regctl image copy "${SRC_IMAGE}" "${DEST_IMAGE}"
;;
esac
done
TAGS_URL=$(echo "${RESP}" | jq -r '.next')
doneNoteDocker Hub automatically creates the destination repository on first push if your account has permission.
Migrate multiple repositories
To migrate several repositories, create a list and run the single-repository script for each one.
For example, create a repos.txt file with repository names:
api
web
workerSave the script from the previous section as migrate-single-repo.sh. Then, run
the following example script that processes each repository in the file. Replace
the environment variable values with your source and destination organization
names.
#!/usr/bin/env bash
set -euo pipefail
SRC_ORG="oldorg"
DEST_ORG="neworg"
while read -r REPO; do
[[ -z "${REPO}" ]] && continue
echo "==== Migrating repo: ${REPO}"
SRC_ORG="${SRC_ORG}" DEST_ORG="${DEST_ORG}" REPO="${REPO}" ./migrate-single-repo.sh
done < repos.txtVerify migration integrity
After copying, verify that source and destination match by comparing digests.
Basic digest verification
The following example script retrieves the image digest for a specific tag from
both source and destination and compares them. If the digests match, the
migration is successful. Replace the environment variable values with your
source and destination organization names, repository name, and tag. You can
choose between crane and regctl for retrieving digests.
#!/usr/bin/env bash
set -euo pipefail
SRC_ORG="oldorg"
DEST_ORG="neworg"
REPO="myapp"
TAG="1.2.3"
SRC_IMAGE="${SRC_ORG}/${REPO}:${TAG}"
DEST_IMAGE="${DEST_ORG}/${REPO}:${TAG}"
# Using crane
SRC_DIGEST=$(crane digest "${SRC_IMAGE}")
DEST_DIGEST=$(crane digest "${DEST_IMAGE}")
# Using regctl (alternative)
# SRC_DIGEST=$(regctl image digest "${SRC_IMAGE}")
# DEST_DIGEST=$(regctl image digest "${DEST_IMAGE}")
echo "Source: ${SRC_DIGEST}"
echo "Destination: ${DEST_DIGEST}"
if [[ "${SRC_DIGEST}" == "${DEST_DIGEST}" ]]; then
echo "✓ Migration verified: digests match"
else
echo "✗ Migration failed: digests do not match"
exit 1
fiMulti-arch verification
For multi-architecture images, also verify the manifest list to ensure all
platforms were copied correctly. Replace the environment variable values with
your source and destination organization names, repository name, and tag. You
can choose between crane and regctl for retrieving manifests.
#!/usr/bin/env bash
set -euo pipefail
SRC_ORG="oldorg"
DEST_ORG="neworg"
REPO="myapp"
TAG="1.2.3"
SRC_IMAGE="${SRC_ORG}/${REPO}:${TAG}"
DEST_IMAGE="${DEST_ORG}/${REPO}:${TAG}"
# Using crane
SRC_MANIFEST=$(crane manifest "${SRC_IMAGE}")
DEST_MANIFEST=$(crane manifest "${DEST_IMAGE}")
# Using regctl (alternative)
# SRC_MANIFEST=$(regctl image manifest --format raw-body "${SRC_IMAGE}")
# DEST_MANIFEST=$(regctl image manifest --format raw-body "${DEST_IMAGE}")
# Check if it's a manifest list (multi-arch)
if echo "${SRC_MANIFEST}" | jq -e '.manifests' > /dev/null 2>&1; then
echo "Multi-arch image detected"
# Compare platform list
SRC_PLATFORMS=$(echo "${SRC_MANIFEST}" | jq -r '.manifests[] | "\(.platform.os)/\(.platform.architecture)"' | sort)
DEST_PLATFORMS=$(echo "${DEST_MANIFEST}" | jq -r '.manifests[] | "\(.platform.os)/\(.platform.architecture)"' | sort)
if [[ "${SRC_PLATFORMS}" == "${DEST_PLATFORMS}" ]]; then
echo "✓ Platform list matches:"
echo "${SRC_PLATFORMS}"
else
echo "✗ Platform lists do not match"
echo "Source platforms:"
echo "${SRC_PLATFORMS}"
echo "Destination platforms:"
echo "${DEST_PLATFORMS}"
exit 1
fi
else
echo "Single-arch image"
fiComplete the migration
After migrating your images, complete these additional steps:
Copy repository metadata in the Docker Hub UI or via API:
- README content
- Repository description
- Topics and tags
Configure repository settings to match the source:
- Visibility (public or private)
- Team permissions and access controls
Reconfigure integrations in the destination organization:
- Webhooks
- Automated builds
- Security scanners
Update image references in your projects:
- Change
FROM oldorg/repo:tagtoFROM neworg/repo:tagin Dockerfiles - Update deployment configurations
- Update documentation
- Change
Deprecate the old location:
- Update the source repository description to point to the new location
- Consider adding a grace period before making the old repository private or read-only