Introduction

Due to some unknown reasons, all Docker mirrors in China was blocked in a day. However, the official source is very slow in China. So I decided to set up my own Docker image mirror.

Unlike a public mirror which needs to mirror every image, I only need to mirror the images I need. So it's much easier to set up a private mirror.

Here will be two steps:

  • Setting up a new Docker registry
  • Setting up a cron job that syncs the images from the official registry to the private registry

That's it!

Based on https://github.com/Joxit/docker-registry-ui

Step 1 - Setting up a new Docker registry

First buy a server from any cloud provider.

Point your domain to it's IP address. For example: hub.aiursoft.cn.

Then install Docker on the server.

ssh user@your_domain.com

Install docker on it:

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

Create a new file with name docker-compose.yml:

sudo touch docker-compose.yml
sudo vim docker-compose.yml

Add the following content:

version: '3.3'

services:
  registry-ui:
    depends_on:
      - registry-server
    image: joxit/docker-registry-ui:main
    ports:
      - target: 8080
        published: 8080
        protocol: tcp
        mode: host
    networks:
      - net
    environment:
      - SINGLE_REGISTRY=true
      - REGISTRY_TITLE=your_domain.com
      - 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

  registry-server:
    image: registry:2.8.2
    volumes:
      - registry-data:/var/lib/registry
    networks:
      - net
    environment:
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: '[https://your_domain.com]'
      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'

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

networks:
  net:
    driver: overlay

Don't forget to replace your_domain.com with your real domain.

Then run the following command:

sudo mkdir -p /registry-data
sudo docker-compose up -d

Now you can visit http://your_domain.com:8080 to see the registry UI.

However, you need to set up a reverse proxy to make it work with HTTPS.

You can install caddy with instruction:

For example, on Debian:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Then create a new file with name Caddyfile:

sudo touch /etc/caddy/Caddyfile
sudo vim /etc/caddy/Caddyfile

Add the following content:

yourdomain.com {
	reverse_proxy http://localhost:8080 {
	}
}

Then restart caddy:

sudo systemctl restart caddy

That's it. Now you can visit https://your_domain.com to see the registry UI.

Step 2 - Setting up a cron job that syncs the images from the official registry to the private registry

To mirror the images, it's simple. You can build a new script like below:

sudo touch /usr/local/bin/sync.sh
sudo chmod +x /usr/local/bin/sync.sh
sudo vim /usr/local/bin/sync.sh

Add the following content:

#!/bin/bash
domain="your_domain.com"

mirror_docker()
{
    containerName=$1

    if [[ $containerName != *":"* ]]; then
        containerName="$containerName:latest"
    fi
    echo "Container name: $containerName"

    docker pull $containerName
    docker rmi $domain/$containerName
    docker tag $containerName $domain/$containerName
    docker push $domain/$containerName
}

mirror_docker "alpine"
mirror_docker "andyzhangx/samba:win-fix"
mirror_docker "ghcr.io/anduin2017/how-to-cook:latest"
mirror_docker "artalk/artalk-go"
mirror_docker "bitwardenrs/server"
mirror_docker "busybox"
mirror_docker "caddy"
mirror_docker "caddy:builder"
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 "dperson/samba"
mirror_docker "elasticsearch:8.11.3"
mirror_docker "edgeneko/neko-image-gallery:edge-cuda11.8"
mirror_docker "edgeneko/neko-image-gallery:edge-cpu"
mirror_docker "frolvlad/alpine-gxx"
mirror_docker "filebrowser/filebrowser"
mirror_docker "gcc"
mirror_docker "gcc:4.9.4"
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 "jellyfin/jellyfin"
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/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/mssql/server"
mirror_docker "mcr.microsoft.com/powershell"
mirror_docker "mediacms/mediacms"
mirror_docker "mediawiki"
mirror_docker "memcached"
mirror_docker "mongo"
mirror_docker "nvidia/cuda:11.6.2-base-ubuntu20.04"
mirror_docker "nvidia/cuda:11.8.0-devel-ubuntu22.04"
mirror_docker "indexyz/mikutap"
mirror_docker "mysql"
mirror_docker "nextcloud"
mirror_docker "nextcloud:stable"
mirror_docker "nextcloud:production"
mirror_docker "nextcloud:27.1.0"
mirror_docker "nginx"
mirror_docker "nginx:alpine"
mirror_docker "node"
mirror_docker "node:16-alpine"
mirror_docker "node:21-alpine"
mirror_docker "openjdk:23-jdk"
mirror_docker "openjdk:8-jdk"
mirror_docker "24oi/oi-wiki"
mirror_docker "owncast/owncast"
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:alpine"
mirror_docker "redis:7-alpine"
mirror_docker "registry:2"
mirror_docker "rigetti/lisp"
mirror_docker "ruby:3.2.2"
mirror_docker "rust:1.74.1"
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:20.04"
mirror_docker "ubuntu:22.04"
mirror_docker "ubuntu:24.04"
mirror_docker "vminnovations/typescript-sdk:16-latest"
mirror_docker "wordpress:php8.3-fpm-alpine"

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

Don't forget to replace your_domain.com with your real domain.

You can run a single command to test the script:

sudo /usr/local/bin/sync.sh

Right now you can see the images are being pulled and pushed to the mirror.

file

Then add a cron job to run the script every day:

sudo crontab -e

Add the following content:

0 0 * * * /usr/local/bin/sync.sh

That's it. Now you have your own Docker image mirror.

Step 3 - Using your own Docker image mirror

To use your own Docker image mirror, instead of pulling from source, you can pull from your own mirror.

For example, if you want to run ubuntu in docker, instead run this:

# With official source
docker run -it                 ubuntu bash
# With your own mirror
docker run -it your_domain.com/ubuntu bash

Or building Dockerfile:

# With official source
FROM ubuntu
# With your own mirror
FROM your_domain.com/ubuntu

Additional Step - Protecting your Docker registry from unauthorized pushes

By default, the Docker registry is open to the public, meaning anyone can push images to your registry. To prevent unauthorized pushes, you can set up a password for your registry.

You can directly do that via caddy. Just add the following content to your Caddyfile:

your_domain.com {
        basicauth / {
                User password-hash
        }
        reverse_proxy https://localhost:8080 {
        }
}

Where password-hash is the hash of your password. You can generate it here: https://bcrypt-generator.com/.

Conclusion

By following the steps outlined above, you have successfully set up your own Docker image mirror. This will significantly speed up your Docker image pulls and ensure that you are not affected by external mirror outages. With your private Docker registry, you have control over which images are mirrored, reducing storage needs and increasing efficiency.

Can now use your private registry to pull images quickly and reliably within China. Enjoy your improved Docker experience!