← Back to DevBytes

Docker Registry Setup: Production Guide

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

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:

  1. Client requests access to a resource
  2. Registry returns a 401 with a realm and service identifier
  3. Client obtains a bearer token from the token service
  4. 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

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:

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

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:

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

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

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles