Use Sonatype Nexus as a Docker Registry Mirror

This guide builds on my earlier posts (read first):

We won’t repeat previous steps. This post only covers: creating the Docker Hub proxy repo, configuring HAProxy on :5002, testing, plus a short note on how Docker “mirrors” work.


What a Docker registry mirror is (quick)

  • A mirror is a pull-through cache in front of an upstream registry (here: Docker Hub).
  • First pull fetches from Hub and caches in Nexus; subsequent pulls come from Nexus at LAN speed.
  • Docker’s daemon-wide "registry-mirrors" applies only to Docker Hub (images pulled without an explicit registry host, e.g., alpine, ubuntu, nginx).
  • For non-Hub registries (GHCR, Quay, ECR, GCR, etc.), you must prefix your Nexus host/port in the image reference.

  1. Create a Docker (proxy) repo to Docker Hub (Nexus UI)

Path: Settings → Repositories → Create repository → docker (proxy)

Change only these fields:

Repository Connectors

  • HTTPPort = 15002 (TLS terminates at HAProxy; leave HTTPS off)

Proxy

  • Remote storagehttps://registry-1.docker.io

(Recommended) HTTP → Authentication (to upstream)

  • Username / Password → your Docker Hub account (reduces rate limiting)

Click Create repository (e.g., name it dockerhub-proxy).
Optional (public pulls): in this repo’s Settings, check Allow anonymous docker pull (Docker Bearer Token Realm must be active—already covered in your previous post).


  1. HAProxy: expose the proxy on :5002 (TLS) and forward to Nexus

Nexus listens on local 15002; HAProxy serves 5002.

/etc/haproxy/haproxy.cfg

# --- Docker proxy on :5002 (TLS at HAProxy, HTTP to Nexus) ---
frontend fe_nexus_docker_proxy_5002
        bind :5002 ssl crt /etc/haproxy/certs/nexus.maksonlee.com.pem alpn http/1.1
        mode http
        option forwardfor
        http-request set-header X-Forwarded-Proto https
        http-request set-header X-Forwarded-Port 5002
        default_backend be_nexus_docker_proxy_5002

backend be_nexus_docker_proxy_5002
        mode http
        balance leastconn
        option httpchk GET /v2/
        # 200 or 401 are both fine for unauthenticated Docker ping
        http-check expect status 200-499
        http-reuse always
        timeout queue   1m
        timeout server  10m
        server nexus_docker_proxy_15002 127.0.0.1:15002 check

Validate & reload:

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy

  1. Test

Reachability (200 or 401 both prove the path works)

curl -I https://nexus.maksonlee.com:5002/v2/

Pull (first time warms the cache)

docker pull nexus.maksonlee.com:5002/library/alpine:latest

  1. Set as a daemon-wide mirror — Docker Hub only

Create /etc/docker/daemon.json and restart:

sudo mkdir -p /etc/docker
printf '{\n  "registry-mirrors": ["https://nexus.maksonlee.com:5002"]\n}\n' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker info | grep -A2 'Registry Mirrors'

Note:

  • • For GHCR/Quay/ECR/GCR, "registry-mirrors" is ignored—use your Nexus proxy by prefixing the host in the image name (e.g., docker pull nexus.maksonlee.com:5003/ORG/IMAGE:TAG).
  • When using a daemon-wide mirror for Docker Hub, if the client has local Docker Hub credentials (docker login, entries for index.docker.io / registry-1.docker.io in ~/.docker/config.json, or a credential helper), Docker may bypass the mirror and contact Hub directly. Remove or isolate Hub creds on machines that must use the mirror: docker logout, clean the Hub entries in ~/.docker/config.json, or use separate DOCKER_CONFIG directories (e.g., /etc/docker/config-mirror vs. /etc/docker/config-hub).

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top