Abstract

Modern IT infrastructures increasingly demand self-hosted solutions that can ensure rapid, secure, and reliable deployment of containerized applications. In response, this work presents a robust and automated system for mirroring Docker images from public repositories to a local registry. The proposed solution tackles the challenges of version discrepancies, image integrity validation, and fault tolerance through a modular architecture that combines multiple Python and shell scripts with standard DevOps tools.

At the heart of the system are three specialized scripts: one to verify if the locally mirrored image is up-to-date by comparing digests of the source and mirror, a second to validate the integrity of an image by parsing and checking its manifest (accommodating both multi-architecture and single-architecture formats), and a third to delete any image that fails integrity checks or is outdated. These are orchestrated by a master shell script which manages the overall mirroring process. This script implements an exponential backoff strategy to gracefully handle transient network or API errors and employs conditional logic to decide whether to force a re-copy of the image when standard procedures do not suffice.

The system’s design addresses several key obstacles: ensuring synchronization consistency between source and mirror, executing precise integrity checks using Docker manifest APIs, and maintaining registry hygiene by cleaning up inconsistent states. Its modular nature not only simplifies maintenance and testing but also enables seamless integration with container orchestration frameworks and periodic scheduling via cron jobs. Consequently, the solution provides a scalable blueprint for local image management in environments where external dependencies must be minimized.

Experimental deployment scenarios demonstrate that the system's rigorous error handling, coupled with its automated cleanup and re-mirroring capabilities, can significantly enhance reliability and performance in production-like settings. This work thereby offers valuable insights into constructing a self-healing, automated image mirroring framework that balances operational efficiency with robust fault tolerance, serving as a practical guideline for advanced DevOps implementations.

Before starting, make sure you have a docker registry server running locally:

Host Registry

file

Registry is a self-hosted docker registry that allows you to store and manage your docker images. It is similar to Docker Hub, but it is self-hosted and can be used for private repositories.

To host Registry on AnduinOS, run the following commands.

First, make sure Docker is installed on your machine. If not, you can install Docker by running the following commands:

curl -fsSL get.docker.com -o get-docker.sh
CHANNEL=stable sh get-docker.sh
rm get-docker.sh

Create a new folder to save the service configuration files:

# Please install Docker first
mkdir -p ~/Source/ServiceConfigs/Registry
cd ~/Source/ServiceConfigs/Registry

Make sure no other process is taking 6157 port on your machine.

function port_exist_check() {
  if [[ 0 -eq $(sudo lsof -i:"$1" | grep -i -c "listen") ]]; then
    echo "$1 is not in use"
    sleep 1
  else
    echo "Warning: $1 is occupied"
    sudo lsof -i:"$1"
    echo "Will kill the occupied process in 5s"
    sleep 5
    sudo lsof -i:"$1" | awk '{print $2}' | grep -v "PID" | sudo xargs kill -9
    echo "Killed the occupied process"
    sleep 1
  fi
}

port_exist_check 8080

Then, create a docker-compose.yml file with the following content:

cat << EOF > ./docker-compose.yml
version: '3.9'

services:
  registry-ui:
    depends_on:
      - registry-server
    image: joxit/docker-registry-ui:main
    ports:
      - target: 80
        published: 8080
        protocol: tcp
        mode: host
    networks:
      - net
    environment:
      - SINGLE_REGISTRY=true
      - REGISTRY_TITLE=My Registry
      - DELETE_IMAGES=true
      - SHOW_CONTENT_DIGEST=true
      - NGINX_PROXY_PASS_URL=http://registry-server:5000
      - SHOW_CATALOG_NB_TAGS=true
      - CATALOG_MIN_BRANCHES=1
      - CATALOG_MAX_BRANCHES=1
      - TAGLIST_PAGE_SIZE=100
      - REGISTRY_SECURED=false
      - CATALOG_ELEMENTS_LIMIT=1000
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
      update_config:
        order: start-first

  registry-server:
    image: registry
    volumes:
      - registry-data:/var/lib/registry
    networks:
      - net
    environment:
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: '[https://hub.aiursoft.cn]' # Replace with your domain
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods: '[HEAD,GET,OPTIONS,DELETE]'
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials: '[true]'
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: '[Authorization,Accept,Cache-Control]'
      REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers: '[Docker-Content-Digest]'
      REGISTRY_STORAGE_DELETE_ENABLED: 'true'
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
      update_config:
        order: start-first

  registry-cleaner:
    depends_on:
      - registry-server
    image: alpine:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    entrypoint:
      - "/bin/sh"
      - "-c"
      - |
        echo "Installing docker";
        apk add --no-cache docker;
        echo "Starting registry cleaner";
        sleep 20;
        docker ps --filter name=registry-server -q | xargs -r -I {} docker exec {} registry garbage-collect /etc/docker/registry/config.yml --delete-untagged=true;
        echo "Garbage collection done. Sleeping infinitely.";
        sleep infinity;
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
      update_config:
        order: start-first

volumes:
  registry-data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /swarm-vol/registry-data

networks:
  net:
    driver: overlay
EOF
sudo mkdir -p /swarm-vol/registry-data

Then, deploy the service:

sudo docker swarm init  --advertise-addr $(hostname -I | awk '{print $1}')
sudo docker stack deploy -c docker-compose.yml registry --detach

That's it! You have successfully hosted registry on AnduinOS.

You can access registry by visiting http://localhost:8080 in your browser.

Uninstall

To uninstall Registry, run the following commands:

sudo docker stack rm registry
sleep 20 # Wait for the stack to be removed
sudo docker system prune -a --volumes -f # Clean up used volumes and images

To also remove the data, log, and config files, run the following commands:

sudo rm /swarm-vol/registry-data -rf

That's it! You have successfully uninstalled Registry from AnduinOS.

Build mirror script

We need to install regctl to mirror the images. Regctl is a command-line tool for managing Docker images and registries.

curl -L https://github.com/regclient/regclient/releases/latest/download/regctl-linux-amd64 >regctl && \
    chmod 755 regctl && \
    mv regctl /usr/local/bin/

And we need 3 python scripts:

  • Check if the image is already latest.
  • Check if the integrity of the image is correct.
  • Delete an image if it is not the latest or the integrity is incorrect.

Check if the image is already latest

Please name the script is_latest.py and save it in the same directory as the other scripts.

#!/usr/bin/env python3
import sys
import subprocess
import os

def get_mirror_image(source_image):
    """Convert a source image name to its mirror equivalent."""
    if ':' in source_image:
        repo_part, tag = source_image.rsplit(':', 1)
    else:
        repo_part = source_image
        tag = "latest"
    
    # Handle special cases for Docker Hub
    if '/' not in repo_part:
        # Simple Docker Hub image like "nginx"
        mirror_repo = f"{repo_part}"
    else:
        # Keep as is for other registries or namespaced Docker Hub images
        mirror_repo = repo_part
    
    ## REPLACE THIS WITH YOUR MIRROR DOMAIN!!!
    return f"hub.aiursoft.cn/{mirror_repo}:{tag}"

def get_image_digest(image):
    """Get the digest for a Docker image using regctl."""
    try:
        result = subprocess.run(
            ["regctl", "image", "digest", image],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            check=False
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout.strip()
        else:
            print(f"Failed to get digest for {image}: {result.stderr}", file=sys.stderr)
            return None
    except Exception as e:
        print(f"Error running regctl for {image}: {e}", file=sys.stderr)
        return None

def image_exists(image):
    """Check if an image exists using regctl."""
    try:
        result = subprocess.run(
            ["regctl", "image", "manifest", image],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            check=False
        )
        return result.returncode == 0
    except:
        return False

def main():
    if len(sys.argv) != 2:
        print("Usage: python is_latest.py <source-image>", file=sys.stderr)
        sys.exit(2)
    
    source_image = sys.argv[1]
    mirror_image = get_mirror_image(source_image)
    
    print(f"Checking if {source_image} is up to date in mirror...", file=sys.stderr)
    print(f"Source: {source_image}", file=sys.stderr)
    print(f"Mirror: {mirror_image}", file=sys.stderr)
    
    # First check if mirror image exists
    if not image_exists(mirror_image):
        print(f"Mirror image {mirror_image} doesn't exist", file=sys.stderr)
        sys.exit(1)  # Not latest - mirror doesn't exist
    
    # Get digests
    source_digest = get_image_digest(source_image)
    mirror_digest = get_image_digest(mirror_image)
    
    if not source_digest or not mirror_digest:
        print("Failed to get digests for comparison", file=sys.stderr)
        sys.exit(1)  # Error, assume not latest
    
    print(f"Source digest: {source_digest}", file=sys.stderr)
    print(f"Mirror digest: {mirror_digest}", file=sys.stderr)
    
    # Compare digests
    if source_digest == mirror_digest:
        print(f"✓ Image {source_image} is up to date in the mirror", file=sys.stderr)
        sys.exit(0)  # True - is latest
    else:
        print(f"✗ Image {source_image} needs to be updated in the mirror", file=sys.stderr)
        sys.exit(1)  # False - not latest

if __name__ == '__main__':
    main()

Check if the integrity of the image is correct

Please name the script check.py and save it in the same directory as the other scripts.

#!/usr/bin/env python3
import sys
import requests

def check_image(image):
    """
    Checking if the image is a valid Aiursoft registry image.
    image format should be:hub.aiursoft.cn/<repository>:<tag>
    """

    ### REPLACE THIS WITH YOUR MIRROR DOMAIN!!!
    registry = "hub.aiursoft.cn"
    if not image.startswith(f"{registry}/"):
        print("Image doesn't belong to registry", file=sys.stderr)
        return False

    try:
        rest = image[len(f"{registry}/"):]
        repository, tag = rest.rsplit(":", 1)
    except ValueError:
        print("Image format error", file=sys.stderr)
        return False

    # Use the tag to get the manifest
    url_tag = f"https://{registry}/v2/{repository}/manifests/{tag}"
    resp = requests.get(url_tag, headers={
        'Accept': 'application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json'
    })
    if not resp.ok:
        print(f"{url_tag} Failed to get manifest: {repository}:{tag}. Status Code: {resp.status_code}", file=sys.stderr)
        return False

    try:
        data = resp.json()
    except Exception as e:
        print("Failed to parse manifest data", file=sys.stderr)
        return False

    # If response contains "manifests" field, it means the image is a multi-arch image
    if "manifests" in data:
        try:
            digest = data["manifests"][0]["digest"]
        except (IndexError, KeyError):
            print("Failed to get digest from manifests", file=sys.stderr)
            return False

        url_digest = f"https://{registry}/v2/{repository}/manifests/{digest}"
        resp_digest = requests.get(url_digest, headers={
            'Accept': 'application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json'
        })
        if not resp_digest.ok:
            print(f"{url_digest} Failed to get manifest: {repository}:{tag}. Status Code: {resp_digest.status_code}", file=sys.stderr)
            return False

    if "manifests" in data:
        print(f"The manifest data contains the 'manifests' field, indicating that this image is a multi-arch image.", file=sys.stderr)
        return True
    elif "layers" in data:
        print(f"The manifest data does not contain the 'manifests' field but contains the 'layers' field, indicating that this image is a single-arch image.", file=sys.stderr)
        return True
    else:
        print(f"The manifest data does not contain 'manifests' and 'layers' fields. URL: {url_tag}. This means something might be wrong!", file=sys.stderr)
        return False

def main():
    if len(sys.argv) < 2:
        print("Usage: check.py <image>", file=sys.stderr)
        sys.exit(2)

    image = sys.argv[1]
    if check_image(image):
        print(f"Image {image} is valid", file=sys.stderr)
        sys.exit(0)
    else:
        sys.exit(1)

if __name__ == '__main__':
    main()

Delete an image if it is not the latest or the integrity is incorrect

Please name the script delete.py and save it in the same directory as the other scripts.

#!/usr/bin/env python3
import sys
import requests

def parse_image(image):
    """
    This function parses the image string to extract the domain, repository, and tag.
    """
    if ':' in image:
        image_part, tag = image.rsplit(':', 1)
    else:
        image_part = image
        tag = "latest"
    if '/' not in image_part:
        print("Error: Image name must be in the format <domain>/<repo>:<tag>")
        sys.exit(1)
    parts = image_part.split('/')
    domain = parts[0]
    repository = '/'.join(parts[1:])

    return domain, repository, tag

def get_digest(domain, repository, tag):
    """
    Call the GET interface to get the manifest of the image.
    """
    url = f"http://{domain}/v2/{repository}/manifests/{tag}"
    headers = {'Accept': 'application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json'}
    print(f"Fetching manifest from {url} ...")
    response = requests.get(url, headers=headers)
    if response.status_code != 200:
        print(f"Error: Unable to fetch manifest. Status code: {response.status_code}")
        print("The image may not exist in the registry.")
        sys.exit(1)
        
    digest = response.headers.get('Docker-Content-Digest')
    if not digest:
        print("Error: Docker-Content-Digest header not found in response.")
        sys.exit(1)
    return digest

def delete_manifest(domain, repository, digest):
    """
    Call the DELETE interface to delete the manifest.
    """
    url = f"http://{domain}/v2/{repository}/manifests/{digest}"
    print(f"Deleting manifest at {url} ...")
    response = requests.delete(url, headers={
        'Accept': 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json'
    })
    if response.status_code == 202:
        print("Deletion successful.")
    else:
        print(f"Error: Deletion failed with status code: {response.status_code}")
        print(response.text)
        sys.exit(1)

def main():
    if len(sys.argv) != 2:
        print("Usage: python ./delete.py <domain>/<repo>:<tag>")
        sys.exit(1)
    image = sys.argv[1]
    domain, repository, tag = parse_image(image)
    print(f"Processing image: {domain}/{repository}:{tag}")
    digest = get_digest(domain, repository, tag)
    print(f"Found digest: {digest}")
    delete_manifest(domain, repository, digest)

if __name__ == '__main__':
    main()

Mirror the image

Please name the script mirror.sh and save it in the same directory as the other scripts.

#!/bin/bash

set -e

try_docker_login() {
    # Load DOCKER_USERNAME from environment variable
    # Load DOCKER_PASSWORD from /run/secrets/docker-password
    # If any of those not set, do not login
    echo "Attempting to login to Docker Hub..."
    echo "Docker USERNAME: $DOCKER_USERNAME"
    DOCKER_PASSWORD=$(cat /run/secrets/DOCKER_PASSWORD 2>/dev/null || echo "")

    if [[ -z "$DOCKER_USERNAME" || -z "$DOCKER_PASSWORD" ]]; then
        echo ">>> Docker credentials are not set. Skipping login."
        return 0
    fi

    echo ">>> Docker credentials are set. Attempting login..."
    echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
}

actual_mirror_docker() {
    sourceImage="$1"
    attempt="$2"

    if [[ "$sourceImage" != *":"* ]]; then
        sourceImage="${sourceImage}:latest"
    fi

    imageName=$(echo "$sourceImage" | cut -d: -f1)
    imageTag=$(echo "$sourceImage" | cut -d: -f2)

    # REPLACE THIS WITH YOUR MIRROR DOMAIN!!!
    finalMirror="hub.aiursoft.cn/${imageName}:${imageTag}"

    echo ">>> Checking if $sourceImage is already mirrored to $finalMirror"
    if python3 is_latest.py "$sourceImage"; then
        echo ">>> Image $sourceImage is already mirrored to $finalMirror. Will check integrity..."
        if ! python3 check.py "$finalMirror"; then
            echo ">>> Integrity check failed for $finalMirror. Attempting to delete..."
            python3 delete.py "$finalMirror"
            return 1
        fi
        echo ">>> Image $finalMirror is valid. Skipping..."
        return 0
    fi

    # If first attempt, use
    if [[ $attempt -eq 1 ]]; then
        regctl image copy "$sourceImage" "$finalMirror" --digest-tags
    else
        # If second or more attempts, use --force-recursive
        regctl image copy "$sourceImage" "$finalMirror" --force-recursive --digest-tags
    fi
    sleep 3

    echo ">>> Image $sourceImage copied to $finalMirror. Checking integrity..."
    
    # First check - manifest via regctl
    if ! regctl image manifest "$finalMirror" &> /dev/null; then
        echo ">>> Manifest check failed for $finalMirror. Attempting to delete..."
        python3 delete.py "$finalMirror"
        return 1
    fi
    
    if ! python3 check.py "$finalMirror"; then
        echo ">>> Health check failed for $finalMirror. Attempting to delete..."
        python3 delete.py "$finalMirror"
        return 1
    fi

    echo ">>> Image $finalMirror is valid. Proceeding to push..."
    return 0
}

mirror_docker() {
    sourceImage="$1"
    max_attempts=8
    
    for attempt in $(seq 1 $max_attempts); do
        echo ">>> Attempting $attempt/$max_attempts: $sourceImage"
        
        if actual_mirror_docker "$sourceImage" "$attempt"; then
            echo ">>> Image $sourceImage mirrored successfully."
            return 0
        fi
        
        # Calculate backoff time with exponential increase and some randomness
        backoff=$((300 + (attempt * attempt * 15) + (RANDOM % 30)))
        
        if [ $attempt -lt $max_attempts ]; then
            echo ">>> Image $sourceImage failed to mirror. Retrying in $backoff seconds..."
            sleep $backoff
        else
            echo ">>> Image $sourceImage failed to mirror after $max_attempts attempts. Skipping..."
            return 1
        fi
    done
}

try_docker_login
mirror_docker "24oi/oi-wiki"
mirror_docker "alpine"
mirror_docker "andyzhangx/samba:win-fix"
mirror_docker "artalk/artalk-go"
mirror_docker "bitwardenrs/server"
mirror_docker "busybox"
mirror_docker "bytemark/webdav"
mirror_docker "caddy"
mirror_docker "caddy:builder"
mirror_docker "clickhouse/clickhouse-server"
mirror_docker "collabora/code"
mirror_docker "consul:1.15.4"
mirror_docker "couchdb"
mirror_docker "couchdb:2.3.0"
mirror_docker "debian:12"
mirror_docker "debian:12.4"
mirror_docker "debian:stable-slim"
mirror_docker "dockurr/windows"
mirror_docker "dperson/samba"
mirror_docker "edgeneko/neko-image-gallery:edge-cpu"
mirror_docker "edgeneko/neko-image-gallery:edge-cuda"
mirror_docker "elasticsearch:8.11.3"
mirror_docker "filebrowser/filebrowser"
mirror_docker "frolvlad/alpine-gxx"
mirror_docker "gcc"
mirror_docker "gcc:4.9.4"
mirror_docker "ghcr.io/anduin2017/how-to-cook:latest"
mirror_docker "ghcr.io/open-webui/open-webui:main"
mirror_docker "ghcr.io/thomiceli/opengist"
mirror_docker "ghcr.io/usememos/memos"
mirror_docker "gitea/gitea"
mirror_docker "gitlab/gitlab-ce"
mirror_docker "gitlab/gitlab-ee"
mirror_docker "golang"
mirror_docker "golang:1.21.5"
mirror_docker "grafana/grafana"
mirror_docker "haproxy"
mirror_docker "haskell"
mirror_docker "haskell:9.8.1"
mirror_docker "hello-world"
mirror_docker "homeassistant/home-assistant"
mirror_docker "httpd"
mirror_docker "immybot/remotely"
mirror_docker "immybot/remotely:69"
mirror_docker "immybot/remotely:88"
mirror_docker "imolein/lua:5.4"
mirror_docker "influxdb"
mirror_docker "influxdb:1.8"
mirror_docker "indexyz/mikutap"
mirror_docker "jellyfin/jellyfin"
mirror_docker "jgraph/drawio:24.7.17"
mirror_docker "joxit/docker-registry-ui"
mirror_docker "jvmilazz0/kavita"
mirror_docker "loicsharma/baget"
mirror_docker "louislam/uptime-kuma"
mirror_docker "mariadb"
mirror_docker "mcr.microsoft.com/azuredataexplorer/kustainer-linux"
mirror_docker "mcr.microsoft.com/dotnet/aspnet:6.0"
mirror_docker "mcr.microsoft.com/dotnet/aspnet:7.0"
mirror_docker "mcr.microsoft.com/dotnet/aspnet:8.0"
mirror_docker "mcr.microsoft.com/dotnet/aspnet:9.0"
mirror_docker "mcr.microsoft.com/dotnet/sdk:6.0"
mirror_docker "mcr.microsoft.com/dotnet/sdk:7.0"
mirror_docker "mcr.microsoft.com/dotnet/sdk:8.0"
mirror_docker "mcr.microsoft.com/dotnet/sdk:9.0"
mirror_docker "mcr.microsoft.com/mssql/server"
mirror_docker "mcr.microsoft.com/powershell"
mirror_docker "mediawiki"
mirror_docker "mediacms/mediacms"
mirror_docker "memcached"
mirror_docker "mongo"
mirror_docker "mysql"
mirror_docker "nextcloud"
mirror_docker "nextcloud:27.1.0"
mirror_docker "nextcloud:production"
mirror_docker "nextcloud:stable"
mirror_docker "nginx"
mirror_docker "nginx:alpine"
mirror_docker "node"
mirror_docker "node:16-alpine"
mirror_docker "node:21-alpine"
mirror_docker "nvidia/cuda:11.6.2-base-ubuntu20.04"
mirror_docker "nvidia/cuda:11.8.0-devel-ubuntu22.04"
mirror_docker "ollama/ollama"
mirror_docker "openjdk:23-jdk"
mirror_docker "openjdk:8-jdk"
mirror_docker "oven/bun:slim"
mirror_docker "owncast/owncast"
mirror_docker "passivelemon/terraria-docker"
mirror_docker "perl:5.39.5"
mirror_docker "phanan/koel"
mirror_docker "php:8.3.0-zts"
mirror_docker "portainer/portainer-ce"
mirror_docker "postgres"
mirror_docker "postgres:14-alpine"
mirror_docker "postgres:15.2-alpine"
mirror_docker "prom/prometheus"
mirror_docker "pytorch/pytorch:2.3.0-cuda11.8-cudnn8-devel"
mirror_docker "python:3.10"
mirror_docker "python:3.11"
mirror_docker "qdrant/qdrant"
mirror_docker "rabbitmq"
mirror_docker "redis"
mirror_docker "redis:7-alpine"
mirror_docker "redis:alpine"
mirror_docker "registry:2"
mirror_docker "rigetti/lisp"
mirror_docker "ruby:3.2.2"
mirror_docker "rust"
mirror_docker "rust:1.81-slim"
mirror_docker "rust:slim"
mirror_docker "rustdesk/rustdesk-server:latest"
mirror_docker "sameersbn/apt-cacher-ng"
mirror_docker "snowdreamtech/frpc"
mirror_docker "snowdreamtech/frps"
mirror_docker "swarmpit/agent"
mirror_docker "swarmpit/swarmpit"
mirror_docker "swift:5.8.1"
mirror_docker "teddysun/xray"
mirror_docker "telegraf"
mirror_docker "thedaviddelta/lingva-translate"
mirror_docker "traefik"
mirror_docker "ubuntu:22.04"
mirror_docker "ubuntu:24.04"
mirror_docker "ubuntu:24.10"
mirror_docker "verdaccio/verdaccio"
mirror_docker "vminnovations/typescript-sdk:16-latest"
mirror_docker "wordpress:php8.3-fpm-alpine"

echo "All images are pulled and pushed to the mirror."

Run the script

Make sure you have python3 and requests installed:

sudo apt-get install python3
pip3 install requests

Make sure the script is executable:

chmod +x mirror.sh

Then, run the script:

./mirror.sh

That's it! You have successfully mirrored the docker images to your local server.

Mirror in docker

You can also run the script in a docker container. Just make sure you have the docker.sock mounted to the container.

FROM ubuntu-with-docker
WORKDIR /
RUN apt update && \
    apt install -y cron python3 python-is-python3 python3-pip curl && \
    rm -rf /var/lib/apt/lists/*

RUN curl -L https://github.com/regclient/regclient/releases/latest/download/regctl-linux-amd64 >regctl && \
    chmod 755 regctl && \
    mv regctl /usr/local/bin/

ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip install requests
      
# Copy entry script
COPY entry.sh /entry.sh
COPY check.py /check.py
COPY delete.py /delete.py
COPY is_latest.py /is_latest.py
RUN chmod +x /entry.sh

# Register a crontab job to run entry every day 3:00
RUN crontab -l | { cat; echo "0 3 * * * /entry.sh"; } | crontab -

# Run this job at the beginning with verbose output
ENTRYPOINT ["cron", "-f", "-L 15"]

And build the image:

docker build -t mirror .

Then, run the image:

docker run -d \
    --name mirror \
    --restart always \
    --network host \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v /run/secrets:/run/secrets \
    mirror

That's it! You have successfully mirrored the docker images to your local server in a docker container.

Using your registry

First, you need to tell docker that the registry might not support HTTPS. You can do this by adding the following line to your /etc/docker/daemon.json file:

{
  "insecure-registries": ["localhost:8080"]
}

Then, restart the docker service:

sudo systemctl restart docker

To use your registry, simply pull the image from your local registry:

docker pull localhost:8080/24oi/oi-wiki

You can also push images to your local registry:

docker tag 24oi/oi-wiki localhost:8080/24oi/oi-wiki
docker push localhost:8080/24oi/oi-wiki

Conclusion

In this guide, we have shown you how to mirror docker images to your local server using a self-hosted docker registry. We have also provided a script to automate the process of mirroring the images and checking their integrity.