Bulk migrate Docker 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.
Prerequisites
Before you begin, ensure you have:
- Docker CLI version 20.10 or later installed
- Docker Buildx (optional but recommended for multi-architecture images)
- Push access to both source and destination organizations
jqinstalled for JSON parsing in scriptscurlfor API calls
Authenticate to Docker Hub
Sign in to Docker Hub to authenticate your session:
$ docker login
Enter your credentials when prompted. This authentication persists for your session and prevents rate limiting issues.
Migrate a single image tag
The basic workflow for migrating a single image tag involves three steps: pull, tag, and push.
Set your source and destination variables:
SRC_ORG=oldorg DEST_ORG=neworg REPO=myapp TAG=1.2.3Pull the image from the source organization:
$ docker pull ${SRC_ORG}/${REPO}:${TAG}Tag the image for the destination organization:
$ docker tag ${SRC_ORG}/${REPO}:${TAG} ${DEST_ORG}/${REPO}:${TAG}Push the image to the destination organization:
$ docker push ${DEST_ORG}/${REPO}:${TAG}
Repeat these steps for any additional tags you need to migrate, including
latest if applicable.
Migrate all tags for a repository
To migrate all tags from a single repository, use this script that queries the Docker Hub API and processes each tag:
#!/usr/bin/env bash
set -euo pipefail
SRC_ORG="oldorg"
DEST_ORG="neworg"
REPO="myapp"
# Paginate through tags
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 "${TAGS_URL}")
echo "${RESP}" | jq -r '.results[].name' | while read -r TAG; do
echo "==> Migrating ${SRC_ORG}/${REPO}:${TAG} → ${DEST_ORG}/${REPO}:${TAG}"
docker pull "${SRC_ORG}/${REPO}:${TAG}"
docker tag "${SRC_ORG}/${REPO}:${TAG}" "${DEST_ORG}/${REPO}:${TAG}"
docker push "${DEST_ORG}/${REPO}:${TAG}"
done
TAGS_URL=$(echo "${RESP}" | jq -r '.next')
doneThis script automatically handles pagination when a repository has more than 100 tags.
NoteDocker Hub automatically creates the destination repository on first push if your account has the necessary permissions.
Migrate private repository tags
For private repositories, authenticate your API calls with a Docker Hub access token:
Create a personal access token in your Docker Hub account settings.
Set your credentials as variables:
HUB_USER="your-username" HUB_TOKEN="your-access-token"Modify the
curlcommand in the script to include authentication:RESP=$(curl -fsSL -u "${HUB_USER}:${HUB_TOKEN}" "${TAGS_URL}")
ImportantIf you encounter pull rate or throughput limits, keep
docker loginactive to avoid anonymous pulls. Consider adding throttling or careful parallelization if migrating large numbers of images.
Migrate multiple repositories
To migrate multiple repositories at once, create a list of repository names and process them in a loop.
Create a file named
repos.txtwith one repository name per line:api web worker databaseSave the single-repository script from the previous section as
migrate-single-repo.shand make it executable.Use this wrapper script to process all repositories:
#!/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}" export REPO ./migrate-single-repo.sh done < repos.txt
Preserve multi-architecture images
Standard docker pull only retrieves the image for your current platform.
For multi-architecture images, this approach loses other platform variants.
Use Buildx imagetools (recommended)
The recommended approach uses Buildx to copy the complete manifest without pulling images locally:
$ docker buildx imagetools create \
-t ${DEST_ORG}/${REPO}:${TAG} \
${SRC_ORG}/${REPO}:${TAG}
This command copies the source manifest with all platforms directly to the destination tag.
Verify the migration by inspecting both manifests:
$ docker buildx imagetools inspect ${SRC_ORG}/${REPO}:${TAG}
$ docker buildx imagetools inspect ${DEST_ORG}/${REPO}:${TAG}
Compare the platforms and digests in the output to confirm they match.
Manual manifest creation
If you need to use the pull/tag/push workflow for multi-architecture images,
you must pull each platform variant and recreate the manifest using
docker manifest create and docker manifest push. This approach is slower
and more error-prone than using Buildx imagetools.
Verify migration integrity
After migrating images, verify that they transferred correctly.
Single-architecture images
Compare image digests between source and destination:
$ docker pull ${SRC_ORG}/${REPO}:${TAG}
$ docker inspect --format='{{index .RepoDigests 0}}' ${SRC_ORG}/${REPO}:${TAG}
$ docker pull ${DEST_ORG}/${REPO}:${TAG}
$ docker inspect --format='{{index .RepoDigests 0}}' ${DEST_ORG}/${REPO}:${TAG}
The SHA256 digests should match if the migration succeeded.
Multi-architecture images
For multi-arch images, compare the output from Buildx imagetools:
$ docker buildx imagetools inspect ${SRC_ORG}/${REPO}:${TAG}
$ docker buildx imagetools inspect ${DEST_ORG}/${REPO}:${TAG}
Verify that the platforms and manifest digest match between source and destination.
Complete 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