Understanding the Docker Registry
A Docker registry is a storage and content delivery system for named Docker images. It holds repositories of tagged image layers, allowing teams to push built images and pull them for deployment across environments. While Docker Hub serves as the default public registry, organizations running production workloads often need a private, self-hosted registry to maintain control, security, and performance.
The registry is a stateless HTTP API server that stores image manifests and layer blobs. Each image is composed of a manifest file describing its layers, and each layer is a compressed tarball identified by a content-addressable digest. The registry validates uploads, handles authentication, and serves downloads efficiently. The official open-source implementation is distribution/distribution, commonly referred to as the Docker Registry v2.
Key Terminology
- Repository: A collection of tagged images for a single application (e.g.,
myapp) - Tag: A mutable pointer to an image manifest (e.g.,
latest,v2.1.3) - Manifest: A JSON document describing which layers compose an image
- Blob / Layer: A content-addressed filesystem delta stored as a compressed tarball
- Digest: A SHA256 hash uniquely identifying a manifest or layer (immutable)
Why a Private Registry Matters in Production
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Relying solely on public registries introduces risks that become unacceptable at scale. A private registry gives you sovereignty over your container supply chain and directly impacts reliability, security, and cost.
Control Over Image Availability
Public registries can experience outages, rate limiting, or policy changes. With an on-premise registry, your CI/CD pipelines and production orchestrators remain unaffected by external dependency failures. Images are always available on your own infrastructure, within your own network latency profile.
Security and Compliance
Organizations subject to regulatory frameworks (HIPAA, PCI-DSS, FedRAMP) often cannot send container images to third-party services. A private registry ensures images never leave your controlled environment. You can integrate with your existing identity providers, enforce image signing, and run vulnerability scanners on images before they enter the registry.
Reduced Data Transfer Costs
Large images pulled from public registries across cloud boundaries incur egress charges. A registry inside your VPC or data center eliminates those recurring costs. Teams pulling frequently—such as Kubernetes nodes during rolling updates—benefit from LAN-speed transfers.
Custom Workflows
A self-hosted registry allows you to implement retention policies, promote images across environments (dev → staging → production), and trigger webhooks that integrate with your existing deployment tooling.
Initial Setup: Running the Registry
The simplest production starting point is running the official registry image as a container with persistent storage. This gives you a functioning v2 registry on localhost.
# Create persistent storage directory
mkdir -p /var/lib/registry/data
# Run the registry container
docker run -d \
--name registry \
--restart always \
-p 5000:5000 \
-v /var/lib/registry/data:/var/lib/registry \
registry:2
Verify it responds correctly:
curl http://localhost:5000/v2/
# Expected output: {}
Now push a test image:
# Tag an existing image for the local registry
docker tag nginx:alpine localhost:5000/nginx:alpine
# Push it
docker push localhost:5000/nginx:alpine
The registry stores uploaded layers under /var/lib/registry/data using a content-addressable layout. At this point the registry is functional but not production-ready—it lacks TLS, authentication, and proper configuration.
Production Configuration File
The registry uses a YAML configuration file located at /etc/docker/registry/config.yml inside the container. A minimal production configuration looks like this:
version: 0.1
log:
accesslog:
disabled: false
level: info
formatter: json
fields:
service: registry
storage:
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
host: https://registry.example.com
secret: generate-a-random-secret-here
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
Mount this configuration file into the container:
docker run -d \
--name registry \
--restart always \
-p 5000:5000 \
-v /var/lib/registry/data:/var/lib/registry \
-v /etc/registry/config.yml:/etc/docker/registry/config.yml \
registry:2
The secret field is used to generate signed HTTP state tokens. Always generate a cryptographically random 32+ byte string—do not reuse or leave it as the default.
TLS Termination: HTTPS in Production
Docker clients refuse to communicate with registries over plain HTTP by default. You must configure TLS. There are two approaches: terminating TLS at the registry itself, or placing the registry behind a reverse proxy that handles TLS.
Option 1: Reverse Proxy (Recommended)
Using Nginx or a cloud load balancer to terminate TLS is the preferred pattern. It separates concerns, allows easier certificate renewal, and lets the registry focus on storage operations. Here is an Nginx configuration for the registry:
server {
listen 443 ssl http2;
server_name registry.example.com;
ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# Allow up to 2GB uploads
client_max_body_size 0;
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $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;
proxy_send_timeout 900;
}
location /v2/ {
proxy_pass http://localhost:5000;
proxy_set_header Host $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;
proxy_send_timeout 900;
}
}
When using a reverse proxy, ensure the registry's http.host configuration matches the external hostname so it can generate correct relative redirect URLs.
Option 2: Direct TLS in the Registry
If you prefer the registry handle TLS directly, configure it with certificate paths:
http:
addr: :5000
host: https://registry.example.com
tls:
certificate: /etc/registry/certs/fullchain.pem
key: /etc/registry/certs/privkey.pem
Mount the certificate directory into the container and ensure the registry process has read permissions on those files.
Authentication and Authorization
Without authentication, anyone who can reach the registry can push and pull images. Production registries must implement access control.
Basic Authentication (HTPasswd)
The simplest method uses an htpasswd file. Generate credentials:
docker run --rm \
--entrypoint htpasswd \
httpd:2 -Bbn myuser mypassword > htpasswd
Update the registry configuration:
auth:
htpasswd:
realm: registry-realm
path: /etc/registry/htpasswd
Mount the file and restart. This is suitable for small teams but does not scale well for dynamic organizations.
Token Authentication (Recommended)
Token-based authentication decouples the registry from the identity provider. The registry delegates authentication to an external token service. The workflow is:
- Client requests access to a resource
- Registry returns a 401 with a realm and service identifier
- Client obtains a bearer token from the token service
- Client retries the request with the token
Configuration for token authentication:
auth:
token:
realm: https://auth.example.com/token
service: registry.example.com
issuer: registry-token-issuer
rootcertbundle: /etc/registry/certs/auth-ca.pem
Several open-source token services exist, including Docker's reference implementation and projects like docker-auth. For production, many teams integrate with their existing OIDC/OAuth2 provider by building a thin token service that validates JWTs issued by their IdP.
Storage Backends
The registry supports multiple storage drivers. Your choice depends on your infrastructure and durability requirements.
Local Filesystem
storage:
filesystem:
rootdirectory: /var/lib/registry
maxthreads: 100
Simple, fast, suitable for single-node setups. Use a persistent volume or dedicated disk. Not suitable for high-availability without shared storage.
Amazon S3
storage:
s3:
accesskey: AKIAIOSFODNN7EXAMPLE
secretkey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region: us-east-1
bucket: my-registry-bucket
encrypt: true
secure: true
v4auth: true
S3 provides durability, replication, and infinite scaling. Use IAM instance profiles instead of static credentials when running on AWS infrastructure. Enable server-side encryption and versioning on the bucket for additional protection.
Google Cloud Storage
storage:
gcs:
bucket: my-registry-bucket
keyfile: /etc/registry/gcs-key.json
rootdirectory: /registry-data
Azure Blob Storage
storage:
azure:
accountname: myregistryaccount
accountkey: base64encodedkey
container: registry-container
realm: core.windows.net
Object Storage Best Practices
- Always encrypt data at rest—enable server-side encryption on the bucket
- Use the cloud provider's IAM for credential management instead of embedding secrets in configuration
- Set lifecycle policies on the bucket to expire old image layers or move them to cheaper storage tiers
- Enable versioning on the bucket to protect against accidental deletion
- Use a dedicated bucket per registry to isolate blast radius
Garbage Collection: Reclaiming Disk Space
The registry stores layers indefinitely. When you delete an image tag or overwrite it with a new build, orphaned layers remain on disk. Garbage collection identifies unreferenced blobs and removes them. Run it with the registry stopped:
# Stop the registry first—never run GC on a live registry
docker stop registry
# Run garbage collection
docker run --rm \
-v /var/lib/registry/data:/var/lib/registry \
-v /etc/registry/config.yml:/etc/docker/registry/config.yml \
registry:2 bin/registry garbage-collect /etc/docker/registry/config.yml
# Start the registry again
docker start registry
Schedule GC as a periodic maintenance task. In high-churn environments, weekly runs are common. The registry must be completely stopped during GC to prevent corruption.
High Availability and Horizontal Scaling
The registry itself is stateless at the HTTP layer, making it horizontally scalable when backed by shared storage. For HA setups:
- Use an object storage backend (S3, GCS, Azure) rather than local filesystem
- Run multiple registry instances behind a load balancer
- Ensure all instances share identical configuration (except for instance-specific identity)
- Use Redis for caching layer locations to improve pull performance
Redis Cache Configuration
storage:
s3:
# ... S3 config ...
cache:
blobdescriptor: redis
redis:
addr: redis-cluster.example.com:6379
db: 0
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
pool:
maxidle: 16
maxactive: 64
The Redis cache stores blob descriptors, reducing S3 list operations and speeding up pulls. It is purely a performance optimization—the source of truth remains in object storage.
Load Balancer Considerations
- Use sticky sessions based on the request path to optimize cache locality (optional)
- Set long proxy timeouts (at least 900 seconds) to accommodate large layer uploads
- Configure health checks against
/v2/endpoint - Disable request buffering on the load balancer for upload streams
Image Retention and Lifecycle Policies
Unbounded registry growth leads to excessive storage costs. Implement automated cleanup policies. The registry itself does not ship with a retention engine, so you must build or adopt tooling.
Using regctl and Scripts
The regctl CLI tool from the regclient project provides powerful image management capabilities:
# List all tags for a repository
regctl tag list registry.example.com/myapp
# Delete a specific tag
regctl tag delete registry.example.com/myapp:v1.old
# Delete tags matching a filter (e.g., all tags older than 30 days)
regctl tag delete registry.example.com/myapp --filter-tag '^v[0-9]+.*' --age 30d
Scheduled Cleanup Script
#!/bin/bash
# cleanup-registry.sh — scheduled via cron weekly
REPO_LIST=("myapp" "frontend" "backend" "worker")
REGISTRY="registry.example.com"
RETENTION_DAYS=90
for repo in "${REPO_LIST[@]}"; do
echo "Cleaning $repo..."
regctl tag delete "$REGISTRY/$repo" --age "${RETENTION_DAYS}d" --keep-min 5
done
# Then run garbage collection
docker stop registry
docker run --rm \
-v /var/lib/registry/data:/var/lib/registry \
registry:2 bin/registry garbage-collect /etc/docker/registry/config.yml
docker start registry
Always keep a minimum number of tags (--keep-min) to preserve recent builds regardless of age.
Monitoring and Observability
A production registry requires monitoring to detect failures, storage exhaustion, and abnormal request patterns.
Health Endpoint
# Registry health check
curl https://registry.example.com/v2/
# Detailed health including storage driver status
curl https://registry.example.com/v2/_catalog
Configure your load balancer or orchestrator to probe /v2/ every 30 seconds.
Prometheus Metrics
Enable Prometheus metrics in the configuration:
http:
addr: :5000
debug:
addr: :5001
prometheus:
enabled: true
path: /metrics
The debug endpoint exposes metrics on a separate port. Scrape http://registry:5001/metrics with Prometheus. Key metrics to alert on:
registry_http_request_duration_seconds— track p95/p99 latencyregistry_http_requests_total— watch for error rate spikesregistry_storage_upload_bytes_total— monitor upload throughputregistry_storage_download_bytes_total— monitor download throughput
Logging
Use structured JSON logging for integration with log aggregation systems:
log:
level: info
formatter: json
fields:
service: registry
environment: production
Ship logs to your centralized logging platform (ELK, Loki, CloudWatch) and create alerts for elevated error rates or authentication failures.
Backup and Disaster Recovery
When using local filesystem storage, regular backups are critical. Object storage backends provide durability, but you should still plan for catastrophic scenarios like bucket deletion.
Filesystem Backup Strategy
#!/bin/bash
# backup-registry.sh — daily backup via cron
BACKUP_DIR="/backups/registry"
DATA_DIR="/var/lib/registry/data"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Stop registry for consistent backup
docker stop registry
tar -czf "$BACKUP_DIR/registry-backup-$TIMESTAMP.tar.gz" -C "$DATA_DIR" .
docker start registry
# Retain last 30 days of backups
find "$BACKUP_DIR" -name "registry-backup-*.tar.gz" -mtime +30 -delete
Test your backups quarterly by restoring to a staging registry and verifying you can pull all critical images.
Object Storage Backup
For S3 backends, enable cross-region replication on the bucket. For GCS, configure dual-region or multi-region buckets. Maintain an inventory of critical image digests so you can repopulate a new registry from build pipelines if necessary.
Image Signing and Trust
In production, you need to verify that images pulled from the registry are the ones your CI system actually built. Docker Content Trust (Notary) provides signing infrastructure.
Notary Integration
Deploy a Notary server alongside your registry. The Notary server stores signed metadata (TUF) that clients can verify:
# Enable content trust on the client
export DOCKER_CONTENT_TRUST=1
# Push with signing
docker push registry.example.com/myapp:production
# Pull with verification
docker pull registry.example.com/myapp:production
# Fails if signature verification fails
Configure the registry to reference the Notary server:
trust:
service:
notary.example.com:
key:
path: /etc/registry/notary-root.pem
Security Hardening Checklist
- TLS everywhere: Never expose the registry over HTTP. Use modern TLS (1.2+) with strong ciphers
- Authentication mandatory: Even internal registries should require credentials to prevent accidental or malicious access
- Network isolation: Place the registry on a private subnet, accessible only via VPN or internal networks
- Read-only access for production clusters: Kubernetes nodes pulling images should have read-only tokens
- Write access restricted to CI systems: Only CI pipelines should push images—never allow developer machines to push directly to production registries
- Regular vulnerability scanning: Integrate Trivy, Grype, or Clair to scan images before and after they enter the registry
- Immutable tags: Consider disabling tag mutation in CI pipelines—use unique version tags or digests for deployments
- Audit logging: Log all push, pull, and delete operations. Ship logs to a tamper-resistant system
- Rate limiting: Configure reverse proxy rate limits to prevent abuse or accidental DoS from misconfigured clients
- Secret rotation: Rotate the registry's HTTP secret, TLS certificates, and cloud credentials on a defined schedule
Integration with Kubernetes
Configure Kubernetes to pull from your private registry using image pull secrets:
# Create a Docker-registry secret
kubectl create secret docker-registry registry-creds \
--docker-server=registry.example.com \
--docker-username=myuser \
--docker-password=mypassword \
--namespace=production
# Reference it in a Pod spec
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: registry.example.com/myapp:latest
imagePullSecrets:
- name: registry-creds
For token-based authentication, configure the kubelet with the token exchange mechanism or use a credential provider plugin that refreshes tokens automatically.
CI/CD Pipeline Integration
A typical pipeline flow with a private registry:
# Build stage
docker build -t registry.example.com/myapp:$CI_COMMIT_SHA .
docker tag registry.example.com/myapp:$CI_COMMIT_SHA registry.example.com/myapp:latest
# Scan stage
trivy image --severity HIGH,CRITICAL registry.example.com/myapp:$CI_COMMIT_SHA
# Push stage
docker push registry.example.com/myapp:$CI_COMMIT_SHA
docker push registry.example.com/myapp:latest
# Deploy stage
kubectl set image deployment/myapp \
myapp=registry.example.com/myapp:$CI_COMMIT_SHA \
--namespace=production
Always push by both digest-derived tag and mutable tag. The digest-derived tag is immutable and safe for deployment rollbacks. Avoid relying solely on latest for production deployments.
Best Practices Summary
- Start with object storage: If you run in the cloud, use S3/GCS/Azure from day one—it eliminates backup concerns and enables HA
- Terminate TLS at a reverse proxy: Simplifies certificate management and allows rate limiting, logging, and WAF integration
- Implement token authentication early: Even for internal registries, authentication protects against accidents and provides audit trails
- Schedule garbage collection: Without GC, storage grows indefinitely. Automate it weekly
- Monitor aggressively: Alert on error rates, storage utilization, and latency anomalies
- Separate registries by environment: Use distinct registries (or at least distinct repositories with strict ACLs) for dev, staging, and production
- Test your backups: A backup you haven't restored is not a backup. Practice disaster recovery quarterly
- Use immutable references in deployment: Deploy by SHA digest or unique version tags—never rely on mutable tags for production
Conclusion
A self-hosted Docker registry is a foundational component of a mature container platform. It gives you complete control over your image supply chain, reduces external dependencies, and integrates naturally with your security and compliance frameworks. The journey from a basic docker run registry:2 to a fully hardened, highly available, authenticated, and monitored registry involves deliberate steps—each adding a layer of reliability and trust. By following the patterns in this guide—TLS termination via reverse proxy, token-based authentication, object storage backends, regular garbage collection, comprehensive monitoring, and disciplined CI/CD integration—you will have a registry infrastructure that supports production workloads with confidence. Remember that the registry is not just storage; it is the distribution hub for every container that runs in your environments. Treat it with the same rigor you apply to any other critical infrastructure service.