BLOG POSTS
How to Set Up a Private Docker Registry on Ubuntu 24

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.

Leave a reply

Your email address will not be published. Required fields are marked