Introduction

While Docker Hub is excellent for public images, production environments require private registries for proprietary applications. A private Docker registry gives you complete control over your container images, their storage location, access permissions, and distribution.

This comprehensive guide covers everything from running a basic local registry to implementing production-grade solutions with authentication, TLS encryption, and high availability.

Why Run a Private Docker Registry?

Security and Compliance:

  • Keep proprietary code and sensitive applications off public registries
  • Meet regulatory requirements for data sovereignty
  • Control who can access your images with fine-grained permissions
  • Scan images for vulnerabilities before deployment

Performance and Reliability:

  • Faster image pulls from your local network or cloud region
  • Reduce dependency on external services
  • Cache frequently used images locally
  • Guaranteed availability for your CI/CD pipelines

Cost Control:

  • Avoid Docker Hub rate limits (100 pulls per 6 hours for anonymous users)
  • No subscription fees for private repositories
  • Control storage costs by managing retention policies

Integration:

  • Seamless integration with CI/CD pipelines
  • Custom webhooks for automation
  • Integration with security scanning tools
  • Support for multiple authentication backends

Part 1: Understanding Docker Registry Architecture

What is a Docker Registry?

A Docker registry is a stateless, server-side application that stores and distributes Docker images. It’s organized into repositories, where each repository contains multiple versions (tags) of an image.

Registry Hierarchy:

Registry (localhost:5000)
├── Repository: my-app
│   ├── Tag: latest
│   ├── Tag: v1.0.0
│   └── Tag: v1.0.1
├── Repository: my-database
│   ├── Tag: latest
│   └── Tag: prod

Registry vs. Repository vs. Image

  • Registry: The service that stores images (e.g., localhost:5000, docker.io)
  • Repository: A collection of related images (e.g., my-app, nginx)
  • Image: A specific version identified by a tag (e.g., my-app:v1.0.0)

Full Image Name Format:

[registry-host]:[port]/[repository]:[tag]

Examples:
localhost:5000/my-app:latest
docker.io/library/nginx:alpine
myregistry.com:443/team/backend:v2.1.0

Part 2: Running a Local Registry (Quick Start)

Step 1: Start the Registry Container

The official Docker registry image makes it incredibly easy to get started:

docker run -d \
  -p 5000:5000 \
  --restart=always \
  --name local-registry \
  registry:2

What this does:

  • -d: Runs in detached (background) mode
  • -p 5000:5000: Maps port 5000 on host to port 5000 in container
  • --restart=always: Automatically restarts if it crashes or on system reboot
  • --name local-registry: Gives the container a memorable name
  • registry:2: Uses the official registry image, version 2

Your registry is now running at localhost:5000!

Step 2: Test the Registry

Check if it’s running:

curl http://localhost:5000/v2/

You should see: {}

This confirms the registry API is accessible.

Part 3: Working with Your Private Registry

Tagging Images for Your Registry

Before pushing an image to your registry, you must tag it with the registry’s address.

Format:

docker tag <source-image> <registry-host>:<port>/<repository>:<tag>

Example:

# Pull a public image
docker pull alpine:latest

# Tag it for your local registry
docker tag alpine:latest localhost:5000/my-alpine:v1.0

# Verify the tag
docker images | grep my-alpine

Pushing Images to Your Registry

docker push localhost:5000/my-alpine:v1.0

What happens:

  1. Docker breaks the image into layers
  2. Each layer is uploaded to the registry
  3. The registry stores the layers and creates a manifest
  4. The manifest maps the tag to the layer IDs

Pulling Images from Your Registry

# Remove local copies to test
docker rmi alpine:latest
docker rmi localhost:5000/my-alpine:v1.0

# Pull from your registry
docker pull localhost:5000/my-alpine:v1.0

# Run it
docker run --rm localhost:5000/my-alpine:v1.0 echo "Hello from private registry!"

Listing Images in Your Registry

The registry doesn’t provide a built-in UI, but you can use the API:

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

# List tags for a specific repository
curl http://localhost:5000/v2/my-alpine/tags/list

Part 4: Production-Ready Registry Setup

The basic registry is great for local testing, but production requires persistence, security, and reliability.

1. Adding Persistent Storage

By default, images are stored inside the container. If you remove the container, your images are gone!

Solution: Mount a volume

docker run -d \
  -p 5000:5000 \
  --restart=always \
  --name persistent-registry \
  -v /mnt/registry-data:/var/lib/registry \
  registry:2

Now your images are stored in /mnt/registry-data on the host and will survive container restarts.

For Docker Compose:

version: '3.8'

services:
  registry:
    image: registry:2
    ports:
      - "5000:5000"
    volumes:
      - registry-data:/var/lib/registry
    restart: always

volumes:
  registry-data:

2. Securing with TLS (HTTPS)

Why TLS is Critical:

  • Docker requires HTTPS for remote registries (unless explicitly marked as insecure)
  • Encrypts image data in transit
  • Prevents man-in-the-middle attacks

Prerequisites:

  • A domain name (e.g., registry.example.com)
  • TLS certificate and private key

Setup:

  1. Obtain TLS certificates (using Let’s Encrypt):
sudo certbot certonly --standalone -d registry.example.com
  1. Run registry with TLS:
docker run -d \
  -p 443:5000 \
  --restart=always \
  --name secure-registry \
  -v /mnt/registry-data:/var/lib/registry \
  -v /etc/letsencrypt/live/registry.example.com:/certs \
  -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/fullchain.pem \
  -e REGISTRY_HTTP_TLS_KEY=/certs/privkey.pem \
  registry:2
  1. Test:
docker pull alpine
docker tag alpine registry.example.com/alpine:latest
docker push registry.example.com/alpine:latest

3. Adding Authentication

Basic Authentication (htpasswd):

  1. Create a password file:
mkdir -p /mnt/registry-auth
docker run --rm --entrypoint htpasswd \
  httpd:2 -Bbn myuser mypassword > /mnt/registry-auth/htpasswd
  1. Run registry with auth:
docker run -d \
  -p 5000:5000 \
  --restart=always \
  --name auth-registry \
  -v /mnt/registry-data:/var/lib/registry \
  -v /mnt/registry-auth:/auth \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  registry:2
  1. Login and push:
docker login localhost:5000
# Username: myuser
# Password: mypassword

docker push localhost:5000/my-app:latest

Part 5: Advanced Registry Solutions

Docker Registry UI

Add a web interface to browse your registry:

version: '3.8'

services:
  registry:
    image: registry:2
    ports:
      - "5000:5000"
    volumes:
      - registry-data:/var/lib/registry
    restart: always

  registry-ui:
    image: joxit/docker-registry-ui:latest
    ports:
      - "8080:80"
    environment:
      - REGISTRY_TITLE=My Private Registry
      - REGISTRY_URL=http://registry:5000
      - DELETE_IMAGES=true
      - SHOW_CONTENT_DIGEST=true
    depends_on:
      - registry
    restart: always

volumes:
  registry-data:

Access the UI at http://localhost:8080

Harbor - Enterprise-Grade Registry

Harbor is a CNCF graduated project that provides:

  • Web UI for image management
  • Role-based access control (RBAC)
  • Vulnerability scanning
  • Image signing and verification
  • Replication across registries
  • Audit logging

Quick Start with Docker Compose:

# Download Harbor installer
wget https://github.com/goharbor/harbor/releases/download/v2.8.0/harbor-offline-installer-v2.8.0.tgz
tar xzvf harbor-offline-installer-v2.8.0.tgz
cd harbor

# Configure
cp harbor.yml.tmpl harbor.yml
# Edit harbor.yml with your settings

# Install
sudo ./install.sh

Access Harbor at https://your-domain (default: admin/Harbor12345)

Part 6: Registry Maintenance and Best Practices

Garbage Collection

Over time, deleted images leave behind unused layers. Run garbage collection to reclaim space:

# Stop the registry
docker stop local-registry

# Run garbage collection
docker run --rm \
  -v /mnt/registry-data:/var/lib/registry \
  registry:2 garbage-collect /etc/docker/registry/config.yml

# Restart the registry
docker start local-registry

Image Retention Policies

Implement policies to automatically delete old images:

# In registry config.yml
storage:
  delete:
    enabled: true
  maintenance:
    uploadpurging:
      enabled: true
      age: 168h  # 7 days
      interval: 24h

Monitoring and Logging

Enable debug logging:

docker run -d \
  -p 5000:5000 \
  -e REGISTRY_LOG_LEVEL=debug \
  registry:2

Prometheus metrics:

# config.yml
http:
  debug:
    addr: :5001
    prometheus:
      enabled: true
      path: /metrics

Backup Strategy

Backup the registry data:

# Stop the registry
docker stop local-registry

# Backup
tar -czf registry-backup-$(date +%Y%m%d).tar.gz /mnt/registry-data

# Restart
docker start local-registry

Part 7: CI/CD Integration

GitLab CI Example

# .gitlab-ci.yml
build:
  stage: build
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Jenkins Pipeline Example

pipeline {
    agent any
    environment {
        REGISTRY = 'registry.example.com'
        IMAGE_NAME = 'my-app'
    }
    stages {
        stage('Build') {
            steps {
                script {
                    docker.build("${REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}")
                }
            }
        }
        stage('Push') {
            steps {
                script {
                    docker.withRegistry("https://${REGISTRY}", 'registry-credentials') {
                        docker.image("${REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}").push()
                    }
                }
            }
        }
    }
}

Conclusion

Running a private Docker registry is essential for production container workflows. This guide covered:

  • Understanding registry architecture and terminology
  • Running a local registry for development
  • Tagging, pushing, and pulling images
  • Production setup with persistence, TLS, and authentication
  • Advanced solutions like Harbor for enterprise needs
  • Maintenance, monitoring, and CI/CD integration

Key Takeaways:

  • Start simple with the official registry image
  • Always use TLS in production
  • Implement authentication and RBAC
  • Regular garbage collection and backups
  • Consider Harbor for enterprise features
  • Integrate with your CI/CD pipeline

With a properly configured private registry, you have full control over your container image lifecycle, from build to deployment.