Share feedback
Answers are generated based on the documentation.

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:

  1. Migrate a single image tag
  2. Migrate all tags for a repository
  3. Migrate multiple repositories

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.

Note

The 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:

  • crane uses Docker credential helpers and ~/.docker/config.json. See the crane documentation.
  • regctl uses 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')
done
Note

Docker 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
worker

Save 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.txt

Verify 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
fi

Multi-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"
fi

Complete the migration

After migrating your images, complete these additional steps:

  1. Copy repository metadata in the Docker Hub UI or via API:

    • README content
    • Repository description
    • Topics and tags
  2. Configure repository settings to match the source:

    • Visibility (public or private)
    • Team permissions and access controls
  3. Reconfigure integrations in the destination organization:

    • Webhooks
    • Automated builds
    • Security scanners
  4. Update image references in your projects:

    • Change FROM oldorg/repo:tag to FROM neworg/repo:tag in Dockerfiles
    • Update deployment configurations
    • Update documentation
  5. 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