
How to Set Up a Private Docker Registry on Ubuntu 24
Setting up a private Docker registry on Ubuntu 24 gives you complete control over your container images, enhanced security, and the ability to keep your proprietary code internal to your organization. Unlike public registries like Docker Hub, a private registry runs on your own infrastructure, eliminating upload/download limits, potential security vulnerabilities from public exposure, and dependency on external services. This guide walks you through the complete process of installing, configuring, and securing your own Docker registry server, including SSL certificate setup, authentication configuration, and performance optimization techniques that’ll have you pushing and pulling images like a pro.
Understanding Docker Registry Architecture
Docker Registry is the server-side application that stores and distributes Docker images. When you run docker pull ubuntu
, you’re actually pulling from Docker’s public registry. The registry speaks the Docker Registry HTTP API V2, which handles image storage, metadata management, and layer deduplication.
The official Docker Registry v2 is a Go-based application that stores images as a collection of layers in a content-addressable storage system. Each image layer gets a unique SHA256 hash, enabling efficient storage through layer sharing between images. The registry can use various storage backends including local filesystem, AWS S3, Google Cloud Storage, or Azure Blob Storage.
Feature | Docker Hub (Free) | Private Registry | Docker Hub (Paid) |
---|---|---|---|
Private repositories | 1 | Unlimited | Unlimited |
Pull rate limits | 200/6h (anonymous), 6000/6h (authenticated) | None | None |
Storage control | No | Full | No |
Custom authentication | No | Yes | Limited |
Network isolation | No | Yes | No |
Prerequisites and System Requirements
Before diving into the setup, make sure your Ubuntu 24 system meets these requirements:
- Ubuntu 24.04 LTS with sudo privileges
- At least 2GB RAM (4GB recommended for production)
- 20GB+ free disk space for image storage
- Docker Engine installed and running
- Domain name pointing to your server (for SSL setup)
- Firewall configured to allow ports 80, 443, and 5000
First, let’s get Docker installed if you haven’t already:
# Update package index
sudo apt update
# Install required packages
sudo apt install apt-transport-https ca-certificates curl software-properties-common
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Add Docker repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io
# Add your user to docker group
sudo usermod -aG docker $USER
# Verify installation
docker --version
Basic Registry Setup
The quickest way to get a registry running is using the official Docker registry image. This basic setup gets you started, though we’ll secure it properly in the next sections.
# Create directory for registry data
sudo mkdir -p /opt/docker-registry/data
sudo mkdir -p /opt/docker-registry/config
# Create basic registry configuration
cat > /opt/docker-registry/config/config.yml << 'EOF'
version: 0.1
log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
EOF
# Run the registry container
docker run -d \
--name docker-registry \
--restart=always \
-p 5000:5000 \
-v /opt/docker-registry/data:/var/lib/registry \
-v /opt/docker-registry/config:/etc/docker/registry \
registry:2
Test your basic registry by tagging and pushing a simple image:
# Pull a small test image
docker pull hello-world
# Tag it for your local registry
docker tag hello-world localhost:5000/hello-world
# Push to your registry
docker push localhost:5000/hello-world
# Verify the push worked
curl -X GET http://localhost:5000/v2/_catalog
You should see a JSON response showing your hello-world repository. However, this setup lacks authentication and SSL, making it unsuitable for production use.
Setting Up SSL/TLS Security
Running a registry without encryption is like leaving your front door wide open. Let's secure it with SSL certificates using Let's Encrypt and Certbot.
# Install Certbot
sudo apt install certbot
# Stop any services using port 80
sudo systemctl stop apache2 nginx 2>/dev/null || true
# Obtain SSL certificate (replace your-domain.com)
sudo certbot certonly --standalone -d registry.your-domain.com
# Create SSL directory for registry
sudo mkdir -p /opt/docker-registry/ssl
# Copy certificates to registry directory
sudo cp /etc/letsencrypt/live/registry.your-domain.com/fullchain.pem /opt/docker-registry/ssl/
sudo cp /etc/letsencrypt/live/registry.your-domain.com/privkey.pem /opt/docker-registry/ssl/
# Set proper permissions
sudo chown -R root:docker /opt/docker-registry/ssl
sudo chmod 640 /opt/docker-registry/ssl/*.pem
Update your registry configuration to use SSL:
cat > /opt/docker-registry/config/config.yml << 'EOF'
version: 0.1
log:
fields:
service: registry
level: info
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
tls:
certificate: /etc/docker/registry/ssl/fullchain.pem
key: /etc/docker/registry/ssl/privkey.pem
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
EOF
# Stop existing registry
docker stop docker-registry
docker rm docker-registry
# Run registry with SSL support
docker run -d \
--name docker-registry \
--restart=always \
-p 443:5000 \
-v /opt/docker-registry/data:/var/lib/registry \
-v /opt/docker-registry/config:/etc/docker/registry \
-v /opt/docker-registry/ssl:/etc/docker/registry/ssl:ro \
registry:2
Implementing Authentication
Basic HTTP authentication provides a simple but effective way to control access to your registry. We'll use htpasswd to create user accounts.
# Install apache2-utils for htpasswd
sudo apt install apache2-utils
# Create auth directory
sudo mkdir -p /opt/docker-registry/auth
# Create first user (replace 'admin' with desired username)
sudo htpasswd -Bc /opt/docker-registry/auth/htpasswd admin
# Add additional users (without -c flag)
sudo htpasswd -B /opt/docker-registry/auth/htpasswd developer
sudo htpasswd -B /opt/docker-registry/auth/htpasswd deployer
# Verify users were created
sudo cat /opt/docker-registry/auth/htpasswd
Update the registry configuration to enable authentication:
cat > /opt/docker-registry/config/config.yml << 'EOF'
version: 0.1
log:
fields:
service: registry
level: info
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
tls:
certificate: /etc/docker/registry/ssl/fullchain.pem
key: /etc/docker/registry/ssl/privkey.pem
auth:
htpasswd:
realm: basic-realm
path: /etc/docker/registry/auth/htpasswd
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
EOF
# Restart registry with authentication
docker stop docker-registry
docker rm docker-registry
docker run -d \
--name docker-registry \
--restart=always \
-p 443:5000 \
-v /opt/docker-registry/data:/var/lib/registry \
-v /opt/docker-registry/config:/etc/docker/registry \
-v /opt/docker-registry/ssl:/etc/docker/registry/ssl:ro \
-v /opt/docker-registry/auth:/etc/docker/registry/auth:ro \
registry:2
Test authentication by logging in and pushing an image:
# Login to your registry
docker login registry.your-domain.com
# Tag and push an image
docker tag hello-world registry.your-domain.com/hello-world
docker push registry.your-domain.com/hello-world
# Test pulling from another machine
docker pull registry.your-domain.com/hello-world
Advanced Configuration and Performance Tuning
For production environments, you'll want to optimize your registry configuration for better performance and reliability. Here's an advanced configuration that includes caching, storage limits, and monitoring.
cat > /opt/docker-registry/config/config.yml << 'EOF'
version: 0.1
log:
accesslog:
disabled: false
level: info
formatter: text
fields:
service: registry
environment: production
storage:
cache:
blobdescriptor: redis
filesystem:
rootdirectory: /var/lib/registry
maxthreads: 100
delete:
enabled: true
redirect:
disable: false
redis:
addr: localhost:6379
password: ""
db: 0
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
http:
addr: :5000
prefix: /
host: https://registry.your-domain.com
secret: asecretforlocaldevelopment
relativeurls: false
draintimeout: 60s
tls:
certificate: /etc/docker/registry/ssl/fullchain.pem
key: /etc/docker/registry/ssl/privkey.pem
minimumtls: tls1.2
headers:
X-Content-Type-Options: [nosniff]
X-Frame-Options: [deny]
X-Content-Security-Policy: [default-src 'none']
http2:
disabled: false
auth:
htpasswd:
realm: "Registry Realm"
path: /etc/docker/registry/auth/htpasswd
validation:
manifests:
urls:
allow:
- ^https?://([^/]+\.)*example\.com/
deny:
- ^https?://www\.example\.com/
proxy:
remoteurl: https://registry-1.docker.io
username: [username]
password: [password]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
file:
- file: /path/to/checked/file
interval: 10s
http:
- uri: /debug/health
headers:
Authorization: [Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==]
statuscode: 200
timeout: 3s
interval: 10s
threshold: 3
reporting:
bugsnag:
apikey: bugsnagapikey
releasestage: production
endpoint: https://bugsnag-endpoint.example.com/
notifications:
events:
includereferences: true
endpoints:
- name: local-5003
url: http://localhost:5003/callback
headers:
Authorization: [Bearer ]
timeout: 1s
threshold: 10
backoff: 1s
disabled: false
EOF
For better performance, install and configure Redis for caching:
# Install Redis
sudo apt install redis-server
# Configure Redis for registry use
sudo sed -i 's/^# maxmemory /maxmemory 256mb/' /etc/redis/redis.conf
sudo sed -i 's/^# maxmemory-policy noeviction/maxmemory-policy allkeys-lru/' /etc/redis/redis.conf
# Start and enable Redis
sudo systemctl start redis-server
sudo systemctl enable redis-server
# Test Redis connection
redis-cli ping
Setting Up Registry Web UI
Managing your registry through command line tools works, but a web interface makes browsing images and managing repositories much easier. We'll use docker-registry-ui, a popular web frontend.
# Create docker-compose file for registry with UI
cat > /opt/docker-registry/docker-compose.yml << 'EOF'
version: '3.8'
services:
registry:
image: registry:2
container_name: docker-registry
restart: always
ports:
- "443:5000"
volumes:
- ./data:/var/lib/registry
- ./config:/etc/docker/registry
- ./ssl:/etc/docker/registry/ssl:ro
- ./auth:/etc/docker/registry/auth:ro
environment:
- REGISTRY_STORAGE_DELETE_ENABLED=true
registry-ui:
image: parabuzzle/craneoperator:latest
container_name: registry-ui
restart: always
ports:
- "8080:80"
environment:
- REGISTRY_HOST=registry
- REGISTRY_PORT=5000
- REGISTRY_PROTOCOL=https
- REGISTRY_USERNAME=admin
- REGISTRY_PASSWORD=your_password_here
depends_on:
- registry
redis:
image: redis:alpine
container_name: registry-redis
restart: always
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
volumes:
redis-data:
EOF
# Start the complete stack
cd /opt/docker-registry
docker-compose up -d
# Check all services are running
docker-compose ps
Real-World Use Cases and Examples
Private Docker registries shine in several scenarios. Here are some practical examples from actual production environments:
CI/CD Pipeline Integration: Many teams integrate private registries directly into their GitLab CI or Jenkins pipelines. Here's a GitLab CI example that builds, tests, and pushes to your private registry:
# .gitlab-ci.yml
stages:
- build
- test
- deploy
variables:
REGISTRY: "registry.your-domain.com"
IMAGE_NAME: "$REGISTRY/$CI_PROJECT_NAME"
IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
before_script:
- docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY
build:
stage: build
script:
- docker build -t $IMAGE_NAME:$IMAGE_TAG .
- docker push $IMAGE_NAME:$IMAGE_TAG
- docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
- docker push $IMAGE_NAME:latest
deploy:
stage: deploy
script:
- docker pull $IMAGE_NAME:$IMAGE_TAG
- docker stop myapp || true
- docker rm myapp || true
- docker run -d --name myapp -p 8080:8080 $IMAGE_NAME:$IMAGE_TAG
only:
- main
Multi-environment Development: Companies often use private registries to maintain separate image versions for development, staging, and production environments:
# Development push
docker tag myapp:latest registry.your-domain.com/myapp:dev-$(git rev-parse --short HEAD)
docker push registry.your-domain.com/myapp:dev-$(git rev-parse --short HEAD)
# Staging promotion
docker pull registry.your-domain.com/myapp:dev-abc123
docker tag registry.your-domain.com/myapp:dev-abc123 registry.your-domain.com/myapp:staging-v1.2.3
docker push registry.your-domain.com/myapp:staging-v1.2.3
# Production release
docker pull registry.your-domain.com/myapp:staging-v1.2.3
docker tag registry.your-domain.com/myapp:staging-v1.2.3 registry.your-domain.com/myapp:production-v1.2.3
docker push registry.your-domain.com/myapp:production-v1.2.3
Storage Backend Options and Scaling
While filesystem storage works for small teams, larger organizations often need more robust storage solutions. Here's how to configure S3-compatible storage:
cat > /opt/docker-registry/config/s3-config.yml << 'EOF'
version: 0.1
log:
fields:
service: registry
storage:
s3:
accesskey: YOUR_ACCESS_KEY
secretkey: YOUR_SECRET_KEY
region: us-west-2
regionendpoint: https://s3.us-west-2.amazonaws.com
bucket: your-registry-bucket
encrypt: true
keyid: alias/your-kms-key
secure: true
v4auth: true
chunksize: 5242880
multipartcopychunksize: 33554432
multipartcopymaxconcurrency: 100
multipartcopythresholdsize: 33554432
rootdirectory: /registry
cache:
blobdescriptor: inmemory
delete:
enabled: true
redirect:
disable: false
EOF
Storage Backend | Best For | Pros | Cons |
---|---|---|---|
Local Filesystem | Small teams, development | Simple, fast access, no external deps | Single point of failure, limited scalability |
AWS S3 | Large scale, high availability | Highly durable, scalable, global CDN | Network latency, costs for frequent access |
Google Cloud Storage | Google Cloud ecosystem | Good integration with GKE, competitive pricing | Vendor lock-in, network dependency |
Azure Blob Storage | Microsoft ecosystem | Seamless Azure integration, enterprise features | Complex pricing, limited to Azure regions |
Monitoring and Maintenance
Production registries need proper monitoring to catch issues before they impact your development workflow. Here's a comprehensive monitoring setup using Prometheus and Grafana:
# Add Prometheus metrics endpoint to registry config
cat >> /opt/docker-registry/config/config.yml << 'EOF'
debug:
addr: :5001
prometheus:
enabled: true
path: /metrics
EOF
# Create monitoring stack with docker-compose
cat > /opt/docker-registry/monitoring.yml << 'EOF'
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: registry-prometheus
restart: always
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
grafana:
image: grafana/grafana:latest
container_name: registry-grafana
restart: always
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin123
volumes:
prometheus-data:
grafana-data:
EOF
# Create Prometheus configuration
cat > /opt/docker-registry/prometheus.yml << 'EOF'
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'docker-registry'
static_configs:
- targets: ['registry:5001']
metrics_path: /metrics
scrape_interval: 30s
- job_name: 'node-exporter'
static_configs:
- targets: ['localhost:9100']
EOF
Set up automated cleanup to prevent disk space issues:
# Create cleanup script
cat > /opt/docker-registry/cleanup.sh << 'EOF'
#!/bin/bash
# Registry cleanup script
REGISTRY_DATA_DIR="/opt/docker-registry/data"
REGISTRY_CONTAINER="docker-registry"
echo "Starting registry cleanup at $(date)"
# Run garbage collection
docker exec $REGISTRY_CONTAINER registry garbage-collect /etc/docker/registry/config.yml
# Remove dangling layers older than 7 days
find $REGISTRY_DATA_DIR -type f -name "data" -mtime +7 -delete
# Check disk usage
df -h $REGISTRY_DATA_DIR
echo "Cleanup completed at $(date)"
EOF
chmod +x /opt/docker-registry/cleanup.sh
# Add to crontab to run weekly
(crontab -l 2>/dev/null; echo "0 2 * * 0 /opt/docker-registry/cleanup.sh >> /var/log/registry-cleanup.log 2>&1") | crontab -
Common Issues and Troubleshooting
Even with careful setup, you'll eventually run into issues. Here are the most common problems and their solutions:
SSL Certificate Issues: The most frequent problem is certificate validation failures. If you're getting "x509: certificate signed by unknown authority" errors:
# Check certificate validity
openssl x509 -in /opt/docker-registry/ssl/fullchain.pem -text -noout
# Verify certificate chain
openssl verify -CAfile /opt/docker-registry/ssl/fullchain.pem /opt/docker-registry/ssl/fullchain.pem
# For self-signed certificates, add to Docker daemon config
sudo mkdir -p /etc/docker/certs.d/registry.your-domain.com
sudo cp /opt/docker-registry/ssl/fullchain.pem /etc/docker/certs.d/registry.your-domain.com/ca.crt
sudo systemctl restart docker
Authentication Failures: If users can't log in, check password file and permissions:
# Verify htpasswd file format
sudo cat /opt/docker-registry/auth/htpasswd
# Test password manually
sudo htpasswd -v /opt/docker-registry/auth/htpasswd username
# Check file permissions
ls -la /opt/docker-registry/auth/htpasswd
# Reset user password if needed
sudo htpasswd -B /opt/docker-registry/auth/htpasswd username
Storage Space Issues: Registries can grow large quickly. Monitor and manage storage proactively:
# Check registry disk usage
du -sh /opt/docker-registry/data
# List largest repositories
find /opt/docker-registry/data -name "current" -exec dirname {} \; | \
xargs -I {} du -sh {} | sort -hr | head -10
# Manual garbage collection
docker exec docker-registry registry garbage-collect /etc/docker/registry/config.yml --dry-run
docker exec docker-registry registry garbage-collect /etc/docker/registry/config.yml
Performance Problems: Slow push/pull operations often indicate network or storage bottlenecks:
# Check registry logs for errors
docker logs docker-registry --tail 100
# Monitor registry metrics
curl -s http://localhost:5001/metrics | grep registry_
# Test network throughput to registry
curl -w "@curl-format.txt" -o /dev/null -s "https://registry.your-domain.com/v2/"
# Create curl timing format file
cat > curl-format.txt << 'EOF'
time_namelookup: %{time_namelookup}\n
time_connect: %{time_connect}\n
time_appconnect: %{time_appconnect}\n
time_pretransfer: %{time_pretransfer}\n
time_redirect: %{time_redirect}\n
time_starttransfer: %{time_starttransfer}\n
----------\n
time_total: %{time_total}\n
EOF
Security Best Practices
Security should be baked into your registry setup from day one. Beyond basic authentication and SSL, consider these additional hardening measures:
# Create non-root user for registry container
cat > /opt/docker-registry/Dockerfile << 'EOF'
FROM registry:2
RUN addgroup -g 1000 registry && \
adduser -D -s /bin/sh -u 1000 -G registry registry
USER registry
EOF
# Build and use hardened registry image
docker build -t secure-registry:latest /opt/docker-registry/
# Configure firewall rules
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
# Set up fail2ban for brute force protection
sudo apt install fail2ban
cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
[docker-registry]
enabled = true
port = https
filter = docker-registry
logpath = /var/log/docker-registry/access.log
maxretry = 3
bantime = 7200
EOF
# Create fail2ban filter for registry
cat > /etc/fail2ban/filter.d/docker-registry.conf << 'EOF'
[Definition]
failregex = ^ - .* "(?:GET|POST|HEAD)" .* 401 .*$
ignoreregex =
EOF
sudo systemctl restart fail2ban
Regular security updates and vulnerability scanning should be part of your maintenance routine:
# Automated security updates
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# Scan registry image for vulnerabilities using Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin
# Scan your registry image
trivy image registry:2
# Scan images in your registry
trivy image registry.your-domain.com/your-app:latest
This comprehensive setup gives you a production-ready Docker registry that's secure, performant, and maintainable. The key to success is starting simple and gradually adding complexity as your needs grow. Whether you're running a small development team or managing enterprise-scale container deployments, this foundation will serve you well. Regular monitoring, proactive maintenance, and keeping security updates current will ensure your registry remains a reliable cornerstone of your container infrastructure.
For hosting your Docker registry, consider using VPS services for smaller deployments or dedicated servers for high-performance, large-scale registry operations. The official Docker Registry documentation at https://docs.docker.com/registry/ provides additional configuration options and advanced deployment scenarios worth exploring as your registry requirements evolve.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.