Docker Registry Private Setup

A private Docker registry allows your organization to store, manage, and distribute container images securely without relying on public registries. This comprehensive guide covers installing, configuring, and maintaining a Docker Registry instance with authentication, TLS encryption, storage backends, and garbage collection. By hosting your own registry, you gain complete control over image distribution, improve deployment speed with local caching, and eliminate external dependencies for CI/CD pipelines.

Table of Contents

Understanding Docker Registry

Docker Registry is the official Docker image distribution system. The registry:2 image provides a stateless, highly scalable application for managing and distributing images. Understanding its architecture is essential for production deployments.

Registry components:

  • Registry API server: Handles image push/pull operations
  • Storage driver: Persists image layers to filesystem, cloud storage, or object storage
  • Authentication layer: Verifies client identity
  • Garbage collection: Removes unreferenced blobs and layers
  • Notification system: Emits events on image operations
# Pull official registry:2 image
docker pull registry:2

# Verify image
docker images | grep registry

# Check registry version
docker run --rm registry:2 /bin/registry --version

Key concepts:

  • Blobs: Individual image layers and configurations
  • Manifests: JSON documents describing image structure
  • Repositories: Collections of related image tags
  • Tags: Human-readable references to specific image versions

Installing Docker Registry

Deploy a registry instance with persistent storage for development and testing environments.

Basic registry setup:

# Create data directory with proper permissions
sudo mkdir -p /opt/registry/data
sudo chown -R 1000:1000 /opt/registry/data
sudo chmod 755 /opt/registry/data

# Run registry container
docker run -d \
  --name registry \
  --restart always \
  -p 5000:5000 \
  -v /opt/registry/data:/var/lib/registry \
  registry:2

# Verify registry is running
docker ps | grep registry

# Test registry health
curl http://localhost:5000/v2/

Configure environment variables for registry:

# Create environment file
cat > /opt/registry/registry.env <<EOF
REGISTRY_LOG_LEVEL=info
REGISTRY_STORAGE_DELETE_ENABLED=true
REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR=inmemory
REGISTRY_HTTP_ADDR=0.0.0.0:5000
REGISTRY_HTTP_RELATIVEURLS=true
EOF

# Run with environment file
docker run -d \
  --name registry \
  --restart always \
  -p 5000:5000 \
  --env-file /opt/registry/registry.env \
  -v /opt/registry/data:/var/lib/registry \
  registry:2

# Verify environment applied
docker exec registry env | grep REGISTRY

Using docker-compose for registry:

# Create docker-compose.yml
cat > /opt/registry/docker-compose.yml <<EOF
version: '3.9'

services:
  registry:
    image: registry:2
    container_name: docker-registry
    restart: always
    ports:
      - "5000:5000"
    volumes:
      - /opt/registry/data:/var/lib/registry
      - /opt/registry/config.yml:/etc/docker/registry/config.yml
    environment:
      REGISTRY_LOG_LEVEL: info
      REGISTRY_STORAGE_DELETE_ENABLED: "true"

EOF

# Start registry
docker-compose -f /opt/registry/docker-compose.yml up -d

# Check logs
docker-compose -f /opt/registry/docker-compose.yml logs -f registry

Configuring TLS/SSL Encryption

TLS encryption is essential for production deployments to protect image transmission and credentials.

Generate self-signed certificates (development only):

# Create certificate directory
sudo mkdir -p /opt/registry/certs
cd /opt/registry/certs

# Generate private key
openssl genrsa -out domain.key 2048

# Generate certificate (valid 365 days)
openssl req -new \
  -x509 \
  -key domain.key \
  -out domain.crt \
  -days 365 \
  -subj "/CN=registry.example.com"

# Verify certificate
openssl x509 -in domain.crt -text -noout

Generate CA-signed certificates (production):

# Create private key
openssl genrsa -out /opt/registry/certs/domain.key 2048

# Create certificate signing request
openssl req -new \
  -key /opt/registry/certs/domain.key \
  -out /opt/registry/certs/domain.csr \
  -subj "/CN=registry.example.com" \
  -addext "subjectAltName=DNS:registry.example.com,DNS:*.registry.example.com,IP:192.168.1.100"

# Submit CSR to CA and receive domain.crt (obtained from your certificate provider)

# Verify certificate chain
openssl verify -CAfile ca.crt domain.crt

Configure registry with TLS:

# Set certificate permissions
sudo chmod 600 /opt/registry/certs/domain.key
sudo chmod 644 /opt/registry/certs/domain.crt

# Run registry with TLS
docker run -d \
  --name registry \
  --restart always \
  -p 443:5000 \
  -v /opt/registry/data:/var/lib/registry \
  -v /opt/registry/certs:/certs \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  registry:2

# Verify HTTPS
curl --cacert /opt/registry/certs/domain.crt https://registry.example.com/v2/

Configure client to trust self-signed certificate:

# Copy certificate to Docker daemon config
sudo mkdir -p /etc/docker/certs.d/registry.example.com:443
sudo cp /opt/registry/certs/domain.crt \
  /etc/docker/certs.d/registry.example.com:443/ca.crt

# Reload Docker daemon
sudo systemctl reload docker

# Test pull/push
docker pull registry.example.com/myimage:latest

Implementing Authentication

Secure your registry with authentication using htpasswd or other methods.

Basic htpasswd authentication:

# Create password file
mkdir -p /opt/registry/auth
docker run --rm \
  --entrypoint htpasswd \
  registry:2 -Bbc /dev/stdout admin password123 > /opt/registry/auth/htpasswd

# Set proper permissions
sudo chmod 600 /opt/registry/auth/htpasswd

# View password file
sudo cat /opt/registry/auth/htpasswd

# Add another user
docker run --rm \
  --entrypoint htpasswd \
  -v /opt/registry/auth:/auth \
  registry:2 -Bbc /auth/htpasswd developer mypassword

Run registry with authentication:

# Stop previous registry
docker rm -f registry

# Run with auth
docker run -d \
  --name registry \
  --restart always \
  -p 5000:5000 \
  -v /opt/registry/data:/var/lib/registry \
  -v /opt/registry/auth:/auth \
  -v /opt/registry/certs:/certs \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  registry:2

# Test authentication
docker login -u admin -p password123 registry.example.com

# Push image
docker tag myimage:latest registry.example.com/myimage:latest
docker push registry.example.com/myimage:latest

# View login credentials
cat ~/.docker/config.json

Token-based authentication (advanced):

# Create auth server configuration
cat > /opt/registry/auth-config.json <<EOF
{
  "server": "https://auth.example.com/token",
  "issuer": "myissuer",
  "rootcertbundle": "/certs/ca.crt"
}
EOF

# Run registry with token auth
docker run -d \
  --name registry \
  --restart always \
  -p 5000:5000 \
  -v /opt/registry/data:/var/lib/registry \
  -v /opt/registry/certs:/certs \
  -e REGISTRY_AUTH=token \
  -e REGISTRY_AUTH_TOKEN_REALM=https://auth.example.com/token \
  -e REGISTRY_AUTH_TOKEN_SERVICE="Docker registry" \
  -e REGISTRY_AUTH_TOKEN_ISSUER=myissuer \
  -e REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/certs/ca.crt \
  registry:2

Storage Backend Configuration

Configure different storage backends based on your infrastructure requirements.

Local filesystem storage (default):

# Already configured in basic setup
# Storage is at /var/lib/registry in container

# Inspect storage
docker exec registry ls -la /var/lib/registry/docker/registry/v2/

AWS S3 storage backend:

# Create registry config with S3 backend
cat > /opt/registry/config.yml <<EOF
version: 0.1
log:
  level: info
storage:
  s3:
    accesskey: AKIAIOSFODNN7EXAMPLE
    secretkey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    region: us-east-1
    bucket: my-docker-registry
    encrypt: true
    secure: true
    v4auth: true
    rootdirectory: /docker-registry
http:
  addr: :5000
  relativeurls: true
EOF

# Run registry with S3
docker run -d \
  --name registry \
  --restart always \
  -p 5000:5000 \
  -v /opt/registry/config.yml:/etc/docker/registry/config.yml \
  registry:2

# Verify S3 connectivity
docker logs registry | grep -i s3

Azure Blob Storage backend:

# Create registry config for Azure
cat > /opt/registry/config.yml <<EOF
version: 0.1
storage:
  azure:
    accountname: myaccount
    accountkey: ACCOUNT_KEY_HERE
    container: docker-registry
    realm: core.windows.net
http:
  addr: :5000
EOF

# Run registry with Azure
docker run -d \
  --name registry \
  --restart always \
  -p 5000:5000 \
  -v /opt/registry/config.yml:/etc/docker/registry/config.yml \
  registry:2

Configure cache layer for performance:

# Create config with Redis cache
cat > /opt/registry/config.yml <<EOF
version: 0.1
storage:
  filesystem:
    rootdirectory: /var/lib/registry
  cache:
    blobdescriptor: redis
  redis:
    addr: redis:6379
    db: 0
    dialtimeout: 10ms
    readtimeout: 10ms
    writetimeout: 10ms
    pool:
      maxidle: 16
      maxactive: 64
      idletimeout: 300s
http:
  addr: :5000
  relativeurls: true
EOF

# Update docker-compose to include Redis
cat > /opt/registry/docker-compose.yml <<EOF
version: '3.9'

services:
  redis:
    image: redis:7-alpine
    container_name: registry-redis
    restart: always
    ports:
      - "6379:6379"

  registry:
    image: registry:2
    container_name: docker-registry
    restart: always
    ports:
      - "5000:5000"
    depends_on:
      - redis
    volumes:
      - /opt/registry/data:/var/lib/registry
      - /opt/registry/config.yml:/etc/docker/registry/config.yml
    environment:
      REGISTRY_LOG_LEVEL: info

EOF

docker-compose -f /opt/registry/docker-compose.yml up -d

Setting Up Nginx Frontend

Nginx provides reverse proxy functionality, load balancing, and additional security for your registry.

Install and configure Nginx:

# Install Nginx
sudo apt-get update
sudo apt-get install -y nginx

# Create Nginx config for registry
sudo tee /etc/nginx/sites-available/registry > /dev/null <<EOF
upstream registry {
    server localhost:5000;
}

server {
    listen 443 ssl http2;
    server_name registry.example.com;

    ssl_certificate /opt/registry/certs/domain.crt;
    ssl_certificate_key /opt/registry/certs/domain.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    client_max_body_size 0;

    location / {
        proxy_pass http://registry;
        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_buffering off;
        proxy_request_buffering off;
    }
}

server {
    listen 80;
    server_name registry.example.com;
    return 301 https://\$server_name\$request_uri;
}
EOF

# Enable site
sudo ln -sf /etc/nginx/sites-available/registry /etc/nginx/sites-enabled/registry

# Test Nginx config
sudo nginx -t

# Start Nginx
sudo systemctl start nginx
sudo systemctl enable nginx

Configure Nginx for caching and compression:

# Update Nginx config with caching
sudo tee /etc/nginx/sites-available/registry > /dev/null <<EOF
upstream registry {
    server localhost:5000;
    keepalive 32;
}

proxy_cache_path /var/cache/nginx/docker-registry levels=1:2 keys_zone=registry_cache:10m max_size=1g inactive=60d use_temp_path=off;

server {
    listen 443 ssl http2;
    server_name registry.example.com;

    ssl_certificate /opt/registry/certs/domain.crt;
    ssl_certificate_key /opt/registry/certs/domain.key;

    gzip on;
    gzip_types application/json;

    client_max_body_size 0;

    location /v2/ {
        proxy_pass http://registry;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;

        # Cache GET requests
        proxy_cache registry_cache;
        proxy_cache_key "\$scheme\$request_method\$host\$request_uri";
        proxy_cache_valid 200 60d;
        proxy_cache_valid 404 10m;
        proxy_cache_bypass \$http_pragma \$http_authorization;
    }
}
EOF

# Reload Nginx
sudo systemctl reload nginx

Managing Images and Garbage Collection

Efficiently manage images and free up disk space with garbage collection.

Manually trigger garbage collection:

# Run garbage collection
docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml

# Check disk usage before
df -h /opt/registry/data

# Run collection with verbose output
docker exec registry bin/registry garbage-collect -d /etc/docker/registry/config.yml

# Check disk usage after
df -h /opt/registry/data

Schedule regular garbage collection:

# Create cron job
sudo crontab -e

# Add line to run daily at 2 AM
0 2 * * * docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml >> /var/log/registry-gc.log 2>&1

# View scheduled jobs
sudo crontab -l

Delete specific images or tags:

# Delete image by manifest digest
curl -X DELETE http://localhost:5000/v2/myimage/manifests/sha256:abc123def456

# List all repositories
curl http://localhost:5000/v2/_catalog

# List tags for repository
curl http://localhost:5000/v2/myimage/tags/list

# Get manifest digest
curl -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
  -X GET http://localhost:5000/v2/myimage/manifests/latest | grep -i docker-content-digest

# Delete old tag
curl -X DELETE \
  http://localhost:5000/v2/myimage/manifests/sha256:abc123...

# Run cleanup after deletion
docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml

Implement retention policies:

# Create cleanup script
cat > /opt/registry/cleanup.sh <<'EOF'
#!/bin/bash

REGISTRY="http://localhost:5000"
KEEP_TAGS=5

for repo in $(curl -s $REGISTRY/v2/_catalog | jq -r '.repositories[]'); do
    echo "Processing $repo..."
    
    # Get all tags sorted by date
    tags=$(curl -s $REGISTRY/v2/$repo/tags/list | jq -r '.tags[] // empty' | sort -r)
    
    tag_count=0
    for tag in $tags; do
        tag_count=$((tag_count + 1))
        if [ $tag_count -gt $KEEP_TAGS ]; then
            # Get manifest digest
            digest=$(curl -I -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
              -s $REGISTRY/v2/$repo/manifests/$tag | grep -i docker-content-digest | cut -d' ' -f2 | tr -d '\r')
            
            # Delete old tag
            curl -X DELETE $REGISTRY/v2/$repo/manifests/$digest
            echo "Deleted $repo:$tag"
        fi
    done
done

# Run garbage collection
docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml
EOF

chmod +x /opt/registry/cleanup.sh

# Run cleanup
/opt/registry/cleanup.sh

High Availability Registry

Configure multiple registry instances for high availability and load distribution.

Setup with load balancer:

# Create docker-compose for multiple registries
cat > /opt/registry/docker-compose-ha.yml <<EOF
version: '3.9'

services:
  registry1:
    image: registry:2
    restart: always
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
      REGISTRY_LOG_LEVEL: info
    volumes:
      - /opt/registry/data:/var/lib/registry
      - /opt/registry/config.yml:/etc/docker/registry/config.yml
    networks:
      - registry-net

  registry2:
    image: registry:2
    restart: always
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
      REGISTRY_LOG_LEVEL: info
    volumes:
      - /opt/registry/data:/var/lib/registry
      - /opt/registry/config.yml:/etc/docker/registry/config.yml
    networks:
      - registry-net

  nginx-lb:
    image: nginx:alpine
    restart: always
    ports:
      - "5000:5000"
    volumes:
      - /opt/registry/nginx-lb.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - registry1
      - registry2
    networks:
      - registry-net

networks:
  registry-net:

EOF

# Create load balancer config
cat > /opt/registry/nginx-lb.conf <<EOF
events {
    worker_connections 1024;
}

http {
    upstream registry_backend {
        server registry1:5000;
        server registry2:5000;
    }

    server {
        listen 5000;
        client_max_body_size 0;

        location / {
            proxy_pass http://registry_backend;
            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_buffering off;
            proxy_request_buffering off;
        }
    }
}
EOF

# Start HA setup
docker-compose -f /opt/registry/docker-compose-ha.yml up -d

# Verify all services
docker-compose -f /opt/registry/docker-compose-ha.yml ps

Monitoring and Maintenance

Monitor registry health, track metrics, and maintain reliable operation.

Enable registry metrics:

# Create config with Prometheus metrics
cat > /opt/registry/config.yml <<EOF
version: 0.1
storage:
  filesystem:
    rootdirectory: /var/lib/registry
http:
  addr: :5000
  relativeurls: true
metrics:
  enabled: true
  prometheus:
    namespace: registry
    subsystem: storage
EOF

# Access metrics endpoint
curl http://localhost:5000/metrics

# Example metrics:
# registry_storage_action_seconds_bucket{action="blobstore_upload",le="+Inf"} 5
# registry_storage_action_seconds_sum{action="blobstore_upload"} 0.25
# registry_storage_action_seconds_count{action="blobstore_upload"} 1

Health check configuration:

# Test registry health
curl -s http://localhost:5000/v2/ && echo "Registry is healthy" || echo "Registry is unhealthy"

# Add health check to container
docker run -d \
  --name registry \
  --restart always \
  -p 5000:5000 \
  --health-cmd='curl -f http://localhost:5000/v2/ || exit 1' \
  --health-interval=30s \
  --health-timeout=10s \
  --health-retries=3 \
  -v /opt/registry/data:/var/lib/registry \
  registry:2

# Check health status
docker inspect registry --format='{{.State.Health.Status}}'

Monitor disk usage and backups:

# Monitor disk usage
du -sh /opt/registry/data

# Set up disk usage alert
cat > /opt/registry/check-disk.sh <<'EOF'
#!/bin/bash
USAGE=$(df /opt/registry/data | awk 'NR==2 {print $5}' | sed 's/%//')
if [ $USAGE -gt 80 ]; then
    echo "WARNING: Registry disk usage at $USAGE%"
    # Run garbage collection
    docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml
fi
EOF

chmod +x /opt/registry/check-disk.sh

# Run periodically
0 */4 * * * /opt/registry/check-disk.sh >> /var/log/registry-disk.log 2>&1

Conclusion

A private Docker registry is an essential component of professional container infrastructure. By implementing TLS encryption, authentication, and configurable storage backends, you create a secure, scalable image distribution system. Regular garbage collection, monitoring, and high availability configurations ensure long-term reliability. Whether you start with basic htpasswd authentication and local storage or scale to multi-instance deployments with S3 backends, the Docker Registry provides a foundation for container image management that supports your organization's growth. Invest time in proper configuration now to avoid operational headaches and security vulnerabilities as your container usage expands.