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 nameregistry: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:
- Docker breaks the image into layers
- Each layer is uploaded to the registry
- The registry stores the layers and creates a manifest
- 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:
- Obtain TLS certificates (using Let’s Encrypt):
sudo certbot certonly --standalone -d registry.example.com
- 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
- 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):
- Create a password file:
mkdir -p /mnt/registry-auth
docker run --rm --entrypoint htpasswd \
httpd:2 -Bbn myuser mypassword > /mnt/registry-auth/htpasswd
- 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
- 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.