Docker Hub is convenient, but you can't always push proprietary code to a public registry. Private registries store images securely within your infrastructure. In this lesson, you'll learn to run your own Docker Registry, implement cleanup policies, and manage images at scale.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Run a private Docker Registry
  • Push and pull images from private registry
  • Set up authentication for registry access
  • Implement image retention policies
  • Use Harbor for enterprise registry features
  • Integrate registry with CI/CD pipelines

2. Why This Matters

Real-world scenario: Your company has proprietary applications. You can't push them to public Docker Hub. A private registry lets you store images securely on your own infrastructure, with access controls and audit logs.

3. Core Concepts

Docker Registry (Basic)

# Run private registry
docker run -d -p 5000:5000 --name registry registry:2

# Tag image for private registry
docker tag myapp:latest localhost:5000/myapp:latest

# Push to private registry
docker push localhost:5000/myapp:latest

# Pull from private registry
docker pull localhost:5000/myapp:latest

# List images in registry
curl http://localhost:5000/v2/_catalog

# List tags for an image
curl http://localhost:5000/v2/myapp/tags/list

# Delete from registry
curl -X DELETE http://localhost:5000/v2/myapp/manifests/<digest>

# Garbage collect (after deletion)
docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml
Basic private registry operations

Secure Registry with Authentication

# Generate htpasswd
mkdir auth
docker run --entrypoint htpasswd httpd:2 -Bbn myuser mypassword > auth/htpasswd

# Run registry with authentication
docker run -d \
  -p 5000:5000 \
  --name registry \
  -v $(pwd)/auth:/auth \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM=Registry \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  -v $(pwd)/certs:/certs \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  registry:2

# Login to registry
docker login localhost:5000

# Docker Compose registry with auth
cat > docker-compose.registry.yml << 'EOF'
version: '3.8'

services:
  registry:
    image: registry:2
    ports:
      - "5000:5000"
    volumes:
      - ./registry-data:/var/lib/registry
      - ./auth:/auth
      - ./certs:/certs
    environment:
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
      REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
      REGISTRY_HTTP_TLS_KEY: /certs/domain.key
    restart: unless-stopped
EOF
Secure registry with authentication and TLS

Harbor: Enterprise Registry

# Install Harbor (production-ready registry)
# Download installer
wget https://github.com/goharbor/harbor/releases/download/v2.10.0/harbor-online-installer-v2.10.0.tgz
tar xvf harbor-online-installer-v2.10.0.tgz
cd harbor

# Configure harbor.yml
# hostname: registry.example.com
# harbor_admin_password: Harbor12345

# Install
./install.sh --with-notary --with-trivy --with-chartmuseum

# Harbor features:
# - Role-based access control
# - Vulnerability scanning (Trivy)
# - Image replication across registries
# - Webhooks for CI/CD integration
# - Helm chart repository
# - Retention policies

# Push to Harbor
docker tag myapp:latest registry.example.com/project/myapp:latest
docker login registry.example.com
docker push registry.example.com/project/myapp:latest

# Harbor CLI (harbor-cli)
# Install: pip install harbor-cli
harbor login https://registry.example.com -u admin -p Harbor12345
harbor project list
harbor project create myproject
harbor artifact list myproject/myapp
Harbor enterprise registry setup

Tagging Strategies

# Tagging best practices

# Git commit SHA (uniquely identifiable)
docker tag myapp registry/myapp:git-sha-abc123def

# Semantic version (human readable)
docker tag myapp registry/myapp:1.2.3
docker tag myapp registry/myapp:v1.2.3

# Environment (deployment target)
docker tag myapp registry/myapp:staging
docker tag myapp registry/myapp:production

# Date-based (for audit)
docker tag myapp registry/myapp:2025-02-07

# Multiple tags per image
docker tag myapp registry/myapp:1.2.3
docker tag myapp registry/myapp:v1.2.3
docker tag myapp registry/myapp:latest
docker tag myapp registry/myapp:git-sha-abc123

docker push registry/myapp --all-tags

# CI/CD tagging
export GIT_SHA=$(git rev-parse --short HEAD)
export BUILD_DATE=$(date -u +%Y%m%d)
docker build -t myapp:$GIT_SHA -t myapp:$BUILD_DATE -t myapp:latest .
Image tagging strategies

Registry Cleanup and Retention

# Manual cleanup
docker system prune -a --volumes

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

# Delete old images from registry (using script)
#!/bin/bash
# delete-old-tags.sh
REGISTRY_URL="localhost:5000"
REPO="myapp"
DAYS=30

cutoff_date=$(date -d "-$DAYS days" +%s)

for tag in $(curl -s https://$REGISTRY_URL/v2/$REPO/tags/list | jq -r '.tags[]'); do
    created_date=$(curl -s https://$REGISTRY_URL/v2/$REPO/manifests/$tag | jq -r '.history[0].v1Compatibility' | jq -r '.created')
    created_epoch=$(date -d "$created_date" +%s)
    if [ $created_epoch -lt $cutoff_date ]; then
        echo "Deleting $tag (created $created_date)"
        curl -X DELETE https://$REGISTRY_URL/v2/$REPO/manifests/$tag
    fi
done

# Harbor retention policies (UI configured)
# Rules:
# - Keep 10 most recent tags
# - Keep tags created in last 30 days
# - Keep tags matching regex 'v\d+\.\d+'
Registry cleanup and retention

4. Complete Project: Enterprise Registry Setup

# docker-compose.full-registry.yml
version: '3.8'

services:
  registry:
    image: registry:2
    ports:
      - "5000:5000"
    volumes:
      - registry_data:/var/lib/registry
      - ./auth:/auth
      - ./certs:/certs
    environment:
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
      REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
      REGISTRY_HTTP_TLS_KEY: /certs/domain.key
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR: redis
      REGISTRY_REDIS_ADDR: redis:6379
    depends_on:
      - redis
    restart: unless-stopped

  redis:
    image: redis:alpine
    restart: unless-stopped

  registry-ui:
    image: joxit/docker-registry-ui:latest
    ports:
      - "8080:80"
    environment:
      - REGISTRY_URL=https://registry:5000
      - REGISTRY_SECURE=true
      - DELETE_IMAGES_ENABLED=true
    depends_on:
      - registry
    restart: unless-stopped

  harbor-core:
    image: goharbor/harbor-core:latest
    depends_on:
      - registry
    environment:
      - DATABASE_TYPE=postgresql
      - POSTGRESQL_HOST=postgres
      - POSTGRESQL_PORT=5432
      - POSTGRESQL_DATABASE=registry
      - POSTGRESQL_USERNAME=harbor
      - POSTGRESQL_PASSWORD=harbor
      - HARBOR_ADMIN_PASSWORD=Harbor12345
    volumes:
      - harbor_data:/data

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: harbor
      POSTGRES_PASSWORD: harbor
      POSTGRES_DB: registry
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  registry_data:
  harbor_data:
  postgres_data:

networks:
  default:
    name: registry-network
Complete enterprise registry stack

CI/CD Integration

# GitHub Actions workflow pushing to private registry
name: Build and Push to Registry

on:
  push:
    branches: [main]
    tags: ['v*']

env:
  REGISTRY: registry.example.com
  IMAGE_NAME: myapp

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Log in to Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix={{sha}}
            type=raw,value=latest,enable={{is_default_branch}}
            type=semver,pattern={{version}}
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  scan:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Scan image with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          format: 'sarif'
          output: 'trivy-results.sarif'
      
      - name: Upload scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'
CI/CD integration with private registry

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next category: Kubernetes - Container Orchestration (Lessons 6.2-6.7)

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen February 7, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment