Setting Up a Private Docker Registry: Complete Self-Hosted Guide
Every docker pull from Docker Hub counts against your rate limit. Every proprietary image pushed to a public registry is a potential security incident waiting to happen. And every CI/CD pipeline that depends on an external registry is one outage away from failing builds.
Running your own Docker registry gives you full control over image storage, access, and distribution. This guide covers everything from a basic setup to a production-hardened deployment with TLS, authentication, cloud storage backends, and automated garbage collection.
Quick Start: Registry in 60 Seconds
The Docker Distribution project (Registry v2) is the official open-source registry:
# Start a basic registry
docker run -d \
--name registry \
-p 5000:5000 \
--restart always \
registry:2
# Tag and push an image
docker tag nginx:alpine localhost:5000/nginx:alpine
docker push localhost:5000/nginx:alpine
# Pull it back
docker pull localhost:5000/nginx:alpine
# List repositories via the API
curl http://localhost:5000/v2/_catalog
# {"repositories":["nginx"]}
This works for local development but is completely unsuitable for production. It has no TLS, no authentication, and stores images in an ephemeral container filesystem. Let us fix all of that.
Production Setup with Docker Compose
Step 1: Generate TLS Certificates
Docker refuses to communicate with registries over plain HTTP (except localhost). You need TLS:
# Using Let's Encrypt with certbot (recommended for public-facing registries)
certbot certonly --standalone -d registry.example.com
# Or generate self-signed certificates for internal use
mkdir -p certs
openssl req -newkey rsa:4096 -nodes -sha256 \
-keyout certs/registry.key \
-x509 -days 365 \
-out certs/registry.crt \
-subj "/CN=registry.example.com" \
-addext "subjectAltName=DNS:registry.example.com,IP:192.168.1.10"
registry.crt to /etc/docker/certs.d/registry.example.com:5000/ca.crt on each client machine.
Step 2: Configure Authentication
The simplest authentication method is htpasswd:
# Create an htpasswd file
mkdir -p auth
docker run --rm --entrypoint htpasswd \
httpd:2 -Bbn admin supersecretpassword > auth/htpasswd
# Add more users
docker run --rm --entrypoint htpasswd \
httpd:2 -Bbn developer devpassword >> auth/htpasswd
Step 3: Registry Configuration File
# config.yml
version: 0.1
log:
level: info
formatter: json
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
cache:
blobdescriptor: inmemory
maintenance:
uploadpurging:
enabled: true
age: 168h
interval: 24h
dryrun: false
http:
addr: :5000
tls:
certificate: /certs/registry.crt
key: /certs/registry.key
headers:
X-Content-Type-Options: [nosniff]
auth:
htpasswd:
realm: "Registry Realm"
path: /auth/htpasswd
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
Step 4: Docker Compose Deployment
# docker-compose.yml
version: "3.8"
services:
registry:
image: registry:2
restart: always
ports:
- "5000:5000"
volumes:
- registry-data:/var/lib/registry
- ./certs:/certs:ro
- ./auth:/auth:ro
- ./config.yml:/etc/docker/registry/config.yml:ro
environment:
REGISTRY_LOG_LEVEL: info
volumes:
registry-data:
# Deploy
docker compose up -d
# Test authentication
docker login registry.example.com:5000
# Username: admin
# Password: supersecretpassword
# Push an image
docker tag myapp:latest registry.example.com:5000/myapp:latest
docker push registry.example.com:5000/myapp:latest
Storage Backends
The filesystem backend is fine for small registries, but for production you should consider cloud storage:
Amazon S3
# config.yml - S3 storage section
storage:
s3:
accesskey: AKIAIOSFODNN7EXAMPLE
secretkey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region: us-east-1
bucket: my-docker-registry
rootdirectory: /registry
encrypt: true
secure: true
v4auth: true
chunksize: 5242880
multipartcopychunksize: 33554432
multipartcopymaxconcurrency: 100
multipartcopythresholdsize: 33554432
delete:
enabled: true
cache:
blobdescriptor: inmemory
Azure Blob Storage
storage:
azure:
accountname: mystorageaccount
accountkey: base64encodedaccountkey
container: docker-registry
realm: core.windows.net
Google Cloud Storage
storage:
gcs:
bucket: my-docker-registry
keyfile: /gcs/keyfile.json
rootdirectory: /registry
chunksize: 5242880
cache.blobdescriptor: redis option with a Redis instance for significantly faster metadata lookups. The in-memory cache does not persist across registry restarts.
Advanced Authentication
Token-Based Authentication
For larger deployments, token-based authentication provides more flexibility than htpasswd:
# config.yml - token auth
auth:
token:
realm: https://auth.example.com/token
service: registry.example.com
issuer: "Auth Service"
rootcertbundle: /certs/auth.crt
autoredirect: false
This delegates authentication to an external service that issues JWT tokens. The registry verifies the token signature against the provided certificate bundle. Open-source implementations include cesanta/docker_auth and Docker's own token auth spec.
LDAP/Active Directory Integration
The native registry does not support LDAP directly. For LDAP integration, use a reverse proxy like Nginx with LDAP auth modules, or switch to Harbor which has built-in LDAP support:
# Nginx configuration with LDAP auth in front of the registry
server {
listen 443 ssl;
server_name registry.example.com;
ssl_certificate /etc/nginx/certs/registry.crt;
ssl_certificate_key /etc/nginx/certs/registry.key;
# Limit upload size for large images
client_max_body_size 0;
# Required for chunked transfer encoding
chunked_transfer_encoding on;
location /v2/ {
auth_basic "Registry Realm";
auth_basic_user_file /etc/nginx/conf.d/htpasswd;
proxy_pass http://registry:5000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900;
}
}
Garbage Collection
When you delete a tag or push a new version of an image, the old layers remain on disk. Garbage collection reclaims this space:
# Dry run first (see what would be deleted)
docker exec registry bin/registry garbage-collect \
/etc/docker/registry/config.yml --dry-run
# Run garbage collection
docker exec registry bin/registry garbage-collect \
/etc/docker/registry/config.yml
# For large registries, stop the registry first to avoid race conditions
docker compose stop registry
docker compose run --rm registry bin/registry garbage-collect \
/etc/docker/registry/config.yml
docker compose start registry
Automate garbage collection with a cron job:
# Run GC weekly at 3 AM on Sundays
0 3 * * 0 docker exec registry bin/registry garbage-collect \
/etc/docker/registry/config.yml >> /var/log/registry-gc.log 2>&1
Registry Web UI
The bare registry has no web interface. Add one with these popular frontends:
Docker Registry UI (Joxit)
# Add to docker-compose.yml
registry-ui:
image: joxit/docker-registry-ui:latest
restart: always
ports:
- "8080:80"
environment:
REGISTRY_TITLE: "My Docker Registry"
REGISTRY_URL: https://registry:5000
DELETE_IMAGES: "true"
SHOW_CONTENT_DIGEST: "true"
NGINX_PROXY_PASS_URL: http://registry:5000
SINGLE_REGISTRY: "true"
depends_on:
- registry
Registry Configuration for UI Access
The registry must return proper CORS headers for the UI to work:
# Add to config.yml under http.headers
http:
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Origin: ["https://registry-ui.example.com"]
Access-Control-Allow-Methods: ["HEAD", "GET", "OPTIONS", "DELETE"]
Access-Control-Allow-Headers: ["Authorization", "Accept", "Cache-Control"]
Access-Control-Expose-Headers: ["Docker-Content-Digest"]
Harbor: The Enterprise Alternative
If you need features beyond what the basic registry provides—vulnerability scanning, RBAC, replication, LDAP/OIDC authentication, audit logs, image signing—consider Harbor:
# Download Harbor installer
wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-online-installer-v2.11.0.tgz
tar xzf harbor-online-installer-v2.11.0.tgz
cd harbor
# Edit harbor.yml
# Set hostname, HTTPS certificates, admin password, database config
# Install
./install.sh --with-trivy --with-chartmuseum
| Feature | Registry v2 | Harbor |
|---|---|---|
| Vulnerability scanning | No | Yes (Trivy integration) |
| RBAC | No | Yes (project-level) |
| Image replication | No | Yes (push and pull modes) |
| LDAP/OIDC auth | No | Yes |
| Web UI | No (third-party) | Yes (built-in) |
| Image signing | Notary (separate) | Cosign/Notation integrated |
| Audit logs | No | Yes |
| Resource usage | ~50MB RAM | ~2GB RAM minimum |
Registry Mirroring and Caching
Configure your registry as a pull-through cache to reduce Docker Hub rate limit hits and speed up image pulls:
# config.yml for a mirror/cache registry
proxy:
remoteurl: https://registry-1.docker.io
username: your-dockerhub-user
password: your-dockerhub-password
# Configure Docker daemon to use the mirror
# /etc/docker/daemon.json
{
"registry-mirrors": ["https://registry.example.com:5000"]
}
# Restart Docker daemon
sudo systemctl restart docker
# Now docker pull nginx:alpine will check your mirror first
docker pull nginx:alpine
Monitoring Your Registry
The registry exposes Prometheus metrics and a debug endpoint:
# config.yml
http:
debug:
addr: :5001
prometheus:
enabled: true
path: /metrics
# Query metrics
curl http://localhost:5001/metrics | grep registry
# Key metrics to monitor:
# registry_storage_action_seconds - Storage operation latency
# registry_http_in_flight_requests - Current active requests
# registry_storage_blob_upload_bytes - Upload throughput
Platforms like usulnet can integrate with your private registry, providing a unified view of both your running containers and the images available in your registry. This makes it straightforward to track which image versions are deployed where.
Security Hardening Checklist
- Always use TLS—never expose a registry over plain HTTP
- Enable authentication with strong passwords or token-based auth
- Set read-only mode for public-facing registries that should not accept pushes:
storage: maintenance: readonly: enabled: true - Restrict network access with firewall rules
- Enable content trust with Docker Content Trust or Cosign
- Scan images for vulnerabilities before pushing (Trivy, Grype)
- Rotate credentials regularly
- Back up the registry storage and test restores
Conclusion
A private Docker registry is a foundational piece of infrastructure for any serious container deployment. Start with the basic Registry v2 for small teams and straightforward use cases. Graduate to Harbor when you need enterprise features like RBAC, vulnerability scanning, and cross-datacenter replication. Whichever you choose, the investment in running your own registry pays dividends in security, reliability, and independence from external services.