Skip to content

How To Transfer GitLab Projects With Container Registry Images

The GitLab Project Transfer tool is used to move projects between different namespaces or groups (you can also move using the gitlab-project-factory). However, this tool will fail when a project has an active Container Registry.

This happens because registry data is stored separately from the repository code, and the transfer process cannot always reconcile existing tags or metadata in the target namespace.

To overcome this, we use a "Donor Repository" strategy. We first copy all images to a temporary "donor" repository to secure the data. This method is chosen over local storage for three reasons:

  1. It tests that the GitLab registry infrastructure can successfully accept the images before the move.
  2. It utilises the enterprise-grade reliability of the server.
  3. It eliminates the risk of hardware failure associated with storing large datasets locally.
  4. Storing critical data on someone's laptop is never a good idea!

Next, we delete the images from the original project's registry to bypass conflict errors, allowing the GitLab transfer to complete successfully. Once the move is finished, we copy the images back from the donor repo into the newly moved project.

Prerequisites

  • Skopeo & jq: Ensure both are installed locally.
  • Credentials: A GitLab Personal Access Token (PAT) with read_registry and write_registry scopes.
  • Permissions: Owner/Maintainer access to both source and target.
brew install skopeo
brew install jq

Create Utility Scripts

Seed Registry

seed-registry.sh - This script will mirror all tags from the source to the target registry:

#!/bin/bash

# --- 1. Environment Validation ---
if [[ -z "$SOURCE" || -z "$DEST" || -z "$CREDS" ]]; then
    echo "āŒ ERROR: Environment variables not set."
    echo "Please run the following before executing:"
    echo '  export SOURCE="registry.path/source"'
    echo '  export DEST="registry.path/destination"'
    echo '  export CREDS="crsid:token"'
    exit 1
fi

echo "--- 2. Discovery Phase ---"
echo "Fetching tag list from: $SOURCE"

# Fetch tags and handle potential connection issues
TAG_LIST=$(skopeo inspect --creds "$CREDS" docker://"$SOURCE" | jq -r '.RepoTags[]' 2>/dev/null)

if [ -z "$TAG_LIST" ]; then
    echo "āŒ ERROR: Could not retrieve tags. Check your path and credentials."
    exit 1
fi

# Output the tags for confirmation
echo "The following tags will be copied:"
echo "------------------------------------------------------------"
echo "$TAG_LIST" | tr '\n' ' ' | sed 's/ $//' | sed 's/ / , /g'
echo -e "\n------------------------------------------------------------"
echo "Total tags found: $(echo "$TAG_LIST" | wc -l)"

# --- 3. Copy Phase ---
echo -e "\n--- 3. Starting Copy Phase ---"
for TAG in $TAG_LIST; do
    echo "šŸš€ Syncing tag: $TAG"
    skopeo copy --multi-arch all \
        --src-creds "$CREDS" \
        --dest-creds "$CREDS" \
        docker://"$SOURCE:$TAG" \
        docker://"$DEST:$TAG"
done

echo -e "\nāœ… Seeding complete! Every tag has been mirrored to $DEST"

Verify Registry Integrity

verify-registry-integrity.sh - This script will verify that all tags in the target registry match those in the source registry:

#!/bin/bash

# --- 1. Environment Check ---
if [[ -z "$SOURCE" || -z "$DEST" || -z "$CREDS" ]]; then
    echo "āŒ ERROR: Environment variables (SOURCE, DEST, CREDS) are not set."
    exit 1
fi

echo "--- Initializing Validation ---"
echo "Source: $SOURCE"
echo "Dest:   $DEST"

# --- 2. Tag Discovery ---
# We fetch tags from the SOURCE to ensure the DEST has everything the SOURCE does.
TAG_LIST=$(skopeo inspect --creds "$CREDS" docker://"$SOURCE" | jq -r '.RepoTags[]')

echo "------------------------------------------------------------"
echo "Validating $(echo "$TAG_LIST" | wc -l) tags..."
echo "------------------------------------------------------------"

# --- 3. Integrity Check Loop ---
MATCH_COUNT=0
FAIL_COUNT=0

for TAG in $TAG_LIST; do
    # Fetching only the Digest (SHA) for speed
    SRC_SHA=$(skopeo inspect --creds "$CREDS" docker://"$SOURCE:$TAG" | jq -r '.Digest')
    DEST_SHA=$(skopeo inspect --creds "$CREDS" docker://"$DEST:$TAG" | jq -r '.Digest' 2>/dev/null)

    if [ "$SRC_SHA" == "$DEST_SHA" ]; then
        echo "āœ… [MATCH] $TAG"
        ((MATCH_COUNT++))
    else
        echo "āŒ [FAIL]  $TAG"
        echo "    Source: $SRC_SHA"
        echo "    Dest:   ${DEST_SHA:-NOT FOUND}"
        ((FAIL_COUNT++))
    fi
done

# --- 4. Summary Report ---
echo "------------------------------------------------------------"
echo "Verification Summary:"
echo "  Success: $MATCH_COUNT tags"
echo "  Failed:  $FAIL_COUNT tags"
echo "------------------------------------------------------------"

if [ $FAIL_COUNT -eq 0 ]; then
    echo "āœ… INTEGRITY VERIFIED: All images match perfectly."
else
    echo "āš ļø  WARNING: Integrity check failed. See details above."
    exit 1
fi
chmod +x seed-registry.sh verify-registry-integrity.sh

Steps

For these steps, we're assuming the following, but obviously you should replace these with the actual paths and image names relevant to your project:

- Source project path: registry.gitlab.developers.cam.ac.uk/group_x/project_x
- Target project path: registry.gitlab.developers.cam.ac.uk/group_y/project_x
- Registry backup project path: registry.gitlab.developers.cam.ac.uk/group_y/registry-backup
- Image name: `my-image`
  1. Create a temporary project called registry-backup in the target namespace. This will be used to hold the mirrored images during the transfer process.
    • we can use an existing donor project for this purpose if one is available, but it is recommended to create a new one to keep things tidy and avoid potential conflicts with existing images.
  2. Set the required environment variables:

    # The full path to the source registry
    export SOURCE="registry.gitlab.developers.cam.ac.uk/group_x/project_x/my-image"
    
    # The full path to the backup registry
    export DEST="registry.gitlab.developers.cam.ac.uk/group_y/registry-backup/my-image"
    
    # Your credentials in CRSid:PAT format
    export CREDS="<CRSid>:<Personal_Access_Token>"
    
  3. Do an initial test to check everything is setup OK by getting the list of tags on the image:

    skopeo inspect --creds "$CREDS" docker://"$SOURCE" | jq -r '.RepoTags[]'
    
  4. Run the seed-registry.sh script to mirror all tags from the source registry to the backup registry:

    ./seed-registry.sh
    
  5. Run the verify-registry-integrity.sh script to verify that all tags in the backup registry match those in the source registry:

    ./verify-registry-integrity.sh
    
  6. If the integrity check fails, try running the seed-registry.sh script again to see if it resolves the issue.

  7. Delete the image(s) from the source registry to prevent conflicts during the transfer process.
  8. Transfer the project using the GitLab Project Transfer tool, moving it from group_x to group_y or do this via gitlab-project-factory if you have it set up.
  9. After the transfer is complete, we need to modify the environment variables to point to the new registry location as the destination and the backup registry as the source:

    # The full path to the backup registry
    export SOURCE="registry.gitlab.developers.cam.ac.uk/group_y/registry-backup/my-image"
    
    # The full path to the target registry
    export DEST="registry.gitlab.developers.cam.ac.uk/group_y/project_x"
    
  10. Run the seed-registry.sh script to mirror all tags from the backup registry to the moved project registry:

    ./seed-registry.sh
    
  11. Run the verify-registry-integrity.sh script to verify that all tags in the moved project registry match those in the backup registry:

    ./verify-registry-integrity.sh
    
  12. If a new backup project was created for this process, delete it to keep things tidy. If an existing donor project was used, you can choose to keep it as a backup in case you need to repeat the process in the future, or delete any images that were copied to it if you don't want to keep them around.