Docker Hub rate-limits anonymous pulls to 100 per 6 hours (200 for authenticated users). In a homelab with frequent rebuilds, multiple hosts, or CI-style workflows, you’ll hit that limit fast. Even if you don’t, pulling the same nginx:alpine image ten times across ten containers wastes bandwidth and time.
A local Docker registry mirror fixes both: it acts as a pull-through cache. First pull is from Docker Hub (or any upstream). Every subsequent pull from any host in your lab hits the local cache instead.
Architecture
┌──────────────────┐
│ Docker Hub │
│ registry-1.dkr │
└────────┬─────────┘
│ upstream
│ (WAN)
┌────────▼─────────┐
│ Registry Mirror │
│ 10.0.20.30:5000 │
│ /var/lib/reg/ │
└────────┬─────────┘
│ local cache
│ (LAN, low latency)
┌────────────┐ ┌────▼────┐ ┌────────────┐
│ SRV1 │ │ SRV2 │ │ LXC/VM │
│ (Proxmox) │ │ (node) │ │ (any host) │
└────────────┘ └─────────┘ └────────────┘
One registry instance on your homelab network. Every Docker daemon is configured to use it as a mirror. Images are stored on a local disk or ZFS dataset.
Deploy the Registry Mirror
Docker Compose
|
|
If you have a Docker Hub authenticated account (recommended for the 200-pull limit), set REGISTRY_PROXY_USERNAME and REGISTRY_PROXY_PASSWORD. Leave them blank for anonymous (100-pull limit).
Health Check
Add a health check to catch issues early:
|
|
Launch and Verify
|
|
Check it’s serving:
|
|
Empty catalog is normal — nothing cached yet. Pull something through the mirror:
|
|
The registry creates the proxy cache on first pull. library/ prefix is the Docker Hub namespace for official images.
Check what’s cached:
|
|
Configure Docker Daemon Clients
Every host that should use the mirror needs its Docker daemon configured. Edit /etc/docker/daemon.json:
|
|
The insecure-registries entry is required because the mirror runs on HTTP (no TLS). If you add TLS later, remove it.
Restart Docker:
|
|
Verify the mirror is in use:
|
|
Test the Mirror
|
|
The second pull should be significantly faster — local network latency vs WAN.
Storage Tuning
ZFS Dataset
If your host uses ZFS, put the registry data on a dedicated dataset with compression:
|
|
Update the compose mount to /var/lib/registry:/var/lib/registry or point to the dataset’s mount point.
Disk Space
A typical homelab sees:
- 10-30 GB after a few weeks of use
- ~50-80 GB for a year of active development with multiple hosts
- More if you pull multi-platform images (arm64 + amd64)
Set a soft limit with a Docker volume size constraint or monitor with du -sh ./data.
GC (Garbage Collection)
Docker registry doesn’t auto-clean blobs when manifests are deleted. To reclaim space:
|
|
Schedule this monthly via cron:
0 3 1 * * cd /path/to/registry && docker compose stop && \
docker run --rm -v ./data:/var/lib/registry registry:2 \
bin/registry garbage-collect /etc/docker/registry/config.yml && \
docker compose start
Multi-Registry Proxy
You’re not limited to Docker Hub. Proxy any registry:
|
|
Run multiple instances on different ports for different upstream registries:
| Service | Port | Upstream |
|---|---|---|
registry-docker |
5000 | Docker Hub |
registry-ghcr |
5001 | ghcr.io |
registry-quay |
5002 | quay.io |
Nginx Reverse Proxy (Optional)
If you want a single entry point with path-based routing to multiple registry instances, put Nginx in front:
|
|
Then configure each Docker daemon with registry-mirrors: ["https://registry.lab/docker"].
Performance Impact
In a real homelab with 3-4 Docker hosts and nightly rebuilds:
| Metric | Without Mirror | With Mirror |
|---|---|---|
| First pull (nginx:alpine ~24 MB) | ~8s (WAN) | ~8s (WAN, first time) |
| Second pull (same host) | ~8s again | ~1s (LAN) |
| Pull across 3 hosts | ~24s total | ~10s total (1 WAN + 2 LAN) |
| API rate limit hit | ~every 2 days with rebuilds | rarely |
| Bandwidth saved (weekly) | — | ~500 MB-2 GB |
The savings compound with every host and every rebuild. For CI runners or developers iterating on Dockerfiles, the mirror is the difference between “waiting for downloads” and “instant.”
Security Notes
- The mirror as described runs HTTP only. On a segmented homelab VLAN (non-routable, no WAN exposure), this is acceptable.
- For multi-user or internet-facing setups, add TLS. The official registry image supports
REGISTRY_HTTP_TLS_CERTIFICATEandREGISTRY_HTTP_TLS_KEY. - The proxy cache stores all pulled images. If you pull private images from a registry, the cached blobs persist until garbage-collected.
Gotchas
Registry-mirrors only catch docker pull from Docker Hub. If your compose files use image: ghcr.io/org/app:latest, that goes directly to ghcr.io unless you configure a second mirror or use the Nginx proxy approach above.
Digest pinning bypasses the mirror. If a pull uses a digest (image@sha256:...) instead of a tag, Docker skips the mirror proxy. Use tags for cache hits.
The registry does not sync proactively. It only caches what you pull. To pre-warm the cache for a new host, run a batch pull script overnight.
Bloat from multi-arch. If your lab mixes amd64 and arm64 (e.g., Raspberry Pi + Proxmox), the registry stores both architectures. This doubles storage. Target your architecture with --platform pulls.
Maintenance Summary
|
|
That’s it. One docker compose up -d, one daemon.json edit per host, and your lab stops hammering Docker Hub.