Skip to content

Docker Registry — Private Registry Setup & Management Guide

DodaTech Updated 2026-06-24 5 min read

In this tutorial, you'll learn about Docker Registry. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Docker Registry is a server-side application for storing and distributing Docker container images, enabling private image storage with access control, Caching, and integration with CI/CD pipelines.

What You'll Learn

Why It Matters

Pushing proprietary container images to public registries (Docker Hub) exposes your intellectual property and introduces dependency on external infrastructure. A private Docker Registry keeps images within your network, reduces pull latency, and gives you full control over access, retention, and storage costs. DodaTech's private registry stores 5,000+ container images across development, staging, and production environments.

Real-World Use

DodaZIP's CI pipeline builds a container image for each microservice, tags it with the Git commit SHA, pushes it to the private Docker Registry, and triggers a rolling Kubernetes deployment. The registry is fronted by a CDN-backed proxy for global pull performance.

flowchart TD
    A[CI Pipeline] --> B[Docker Build]
    B --> C[Docker Registry]
    C --> D[Storage Backend]
    D --> E[S3 / GCS / Azure]
    C --> F[Authentication]
    F --> G[htpasswd / OAuth2]
    C --> H[TLS Termination]
    C --> I[Kubernetes Pull]
    I --> J[Node 1]
    I --> K[Node 2]
    I --> L[Node N]
    style C fill:#2496ED,color:#fff
â„šī¸ Info

Prerequisites: Docker installed. Familiarity with TLS certificates and storage backends.

Installation

# Run the official Docker Registry
docker run -d \
  --name docker-registry \
  -p 5000:5000 \
  --restart always \
  -v registry_data:/var/lib/registry \
  registry:2.8.3

# Expected output:
# Container ID returned

# Verify
curl http://localhost:5000/v2/

# Expected output:
# {}

# Check API version
curl http://localhost:5000/v2/_catalog

# Expected output:
# {"repositories":[]}

Configuration with Docker Compose

# docker-compose.yml
version: '3.8'

services:
  registry:
    image: registry:2.8.3
    ports:
      - "5000:5000"
      - "5001:5001"
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
      REGISTRY_HTTP_SECRET: changethissecret
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      REGISTRY_COMPATIBILITY_SCHEMA1_ENABLED: "false"
    volumes:
      - registry_data:/data
      - ./certs:/certs
      - ./auth:/auth
      - ./config.yml:/etc/docker/registry/config.yml
    restart: always

volumes:
  registry_data:
# config.yml
version: 0.1
log:
  level: info
  fields:
    service: registry
storage:
  cache:
    blobdescriptor: inmemory
  filesystem:
    rootdirectory: /data
  delete:
    enabled: true
http:
  addr: :5000
  secret: changethissecret
  headers:
    X-Content-Type-Options: [nosniff]
    Access-Control-Allow-Origin: ['*']
    Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
  tls:
    certificate: /certs/registry.crt
    key: /certs/registry.key
auth:
  htpasswd:
    realm: basic-realm
    path: /auth/htpasswd
health:
  storagedriver:
    enabled: true
    interval: 10s
    threshold: 3

Authentication Setup

# Create htpasswd file with bcrypt
docker run --entrypoint htpasswd \
  httpd:2.4 -Bbn admin SecureP@ss1 > auth/htpasswd

# Add another user
docker run --entrypoint htpasswd \
  httpd:2.4 -Bbn deploy-team DeployP@ss2 >> auth/htpasswd

# Verify the file
cat auth/htpasswd

# Expected output:
# admin:$2y$05$abc...
# deploy-team:$2y$05$def...

# Restart registry to pick up auth
docker compose restart registry
# Test authentication
curl -u admin:SecureP@ss1 http://localhost:5000/v2/_catalog

# Expected output:
# {"repositories":[]}

# Without auth (should fail)
curl http://localhost:5000/v2/_catalog

# Expected output:
# {"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}

Storage Backend Configuration

# config.yml — S3 storage backend
storage:
  s3:
    accesskey: AKIAIOSFODNN7EXAMPLE
    secretkey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    region: us-east-1
    bucket: dodatech-registry
    rootdirectory: /docker/registry
    encrypt: true
    keyid: arn:aws:kms:us-east-1:123456789012:key/abc123
    secure: true
    skipverify: false
    v4auth: true
    multipart: true
    chunksize: "5242880"  # 5MB chunks for uploads
# config.yml — GCS storage backend
storage:
  gcs:
    bucket: dodatech-registry
    keyfile: /gcs-key.json
    rootdirectory: /docker/registry
    chunksize: 5242880
# config.yml — Azure storage backend
storage:
  azure:
    accountname: dodatechregistry
    accountkey: your-azure-storage-key
    container: registry
    realm: core.windows.net

Push and Pull Images

# Login to private registry
docker login registry.dodatech.com:5000 -u admin -p SecureP@ss1

# Expected output:
# Login Succeeded

# Tag and push an image
docker tag dodazip:latest registry.dodatech.com:5000/dodazip:1.0.0
docker push registry.dodatech.com:5000/dodazip:1.0.0

# Expected output:
# The push refers to repository [registry.dodatech.com:5000/dodazip]
# 2a3b5c7d8e9f: Pushed
# 3b4c6d8e0f1a: Pushed
# 1.0.0: digest: sha256:abc123... size: 1784

# Pull from private registry
docker pull registry.dodatech.com:5000/dodazip:1.0.0

# List tags
curl -u admin:SecureP@ss1 \
  http://localhost:5000/v2/dodazip/tags/list

# Expected output:
# {"name":"dodazip","tags":["1.0.0","latest","1.0.1-rc1"]}

Garbage Collection

# Enable delete in config.yml first (storage.delete.enabled: true)

# Delete a manifest
curl -u admin:SecureP@ss1 -X DELETE \
  http://localhost:5000/v2/dodazip/manifests/sha256:abc123

# Expected output:
# 202 Accepted

# Run garbage collection (inside the container)
docker exec registry-registry-1 \
  registry garbage-collect /etc/docker/registry/config.yml

# Expected output:
# blobs marked for deletion: 15
# blobs deleted: 12
# blobs that cannot be deleted: 0

# Dry run first
docker exec registry-registry-1 \
  registry garbage-collect --dry-run /etc/docker/registry/config.yml

CI/CD Integration

# .github/workflows/docker-publish.yml
name: Build and Push

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

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Log in to private registry
        uses: docker/login-action@v3
        with:
          registry: registry.dodatech.com:5000
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
tags: |
            registry.dodatech.com:5000/dodazip:latest
            registry.dodatech.com:5000/dodazip:${{ github.sha }}
            registry.dodatech.com:5000/dodazip:${{ github.ref_name }}
# Kubernetes pull secret
apiVersion: v1
kind: Secret
metadata:
  name: registry-credentials
  namespace: production
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5kb2RhdGVjaC5jb206NTAwMCI6eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJTZWN1cmVQQHNzMSIsImF1dGgiOiJZV1J0YVc0NmRHNW5kR2x6ZEhOemRITT0ifX19
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dodazip
  namespace: production
spec:
  template:
    spec:
      imagePullSecrets:
        - name: registry-credentials
      containers:
        - name: dodazip
          image: registry.dodatech.com:5000/dodazip:1.0.0

Common Configuration Mistakes

  1. Running registry without TLS: Docker requires HTTPS for registries (except localhost). Without TLS, clients refuse to push/pull. Use a reverse proxy (NGINX, Traefik) or configure TLS directly.

  2. Not enabling Garbage Collection: Deleted manifests leave orphaned blob layers on disk. Schedule regular Garbage Collection to reclaim storage space.

  3. Using the filesystem backend on ephemeral storage: Pod restarts delete all images. Use S3, GCS, or Azure Blob storage for durable, scalable storage.

  4. Missing delete.enabled: true for cleanup: Without this flag, the API rejects DELETE requests and Garbage Collection cannot remove layers.

  5. Weak http.secret value: The secret is used to sign upload tokens. A weak or default secret allows token forgery. Generate a random 64-character string.

Practice Questions

  1. What is the difference between a Docker Registry and a Docker Hub? Answer: Docker Registry is the server software for storing images. Docker Hub is a hosted service running Docker Registry with additional features like automated builds and teams.

  2. Why is TLS required for Docker registries? Answer: Docker clients enforce TLS for non-localhost registries to prevent man-in-the-middle attacks and credential interception.

  3. How does Garbage Collection work in Docker Registry? Answer: GC scans blob storage, identifies unreferenced blob layers (no manifest points to them), and deletes them. It requires storage.delete.enabled: true.

  4. What storage backends does Docker Registry support? Answer: Filesystem (local), S3 (AWS), GCS (Google), Azure Blob, Swift (OpenStack), and Alibaba OSS via configuration.

Challenge

Deploy a production-grade Docker Registry: configure TLS with Let's Encrypt certificates, set up htpasswd authentication with bcrypt, use S3 as the storage backend, enable delete and configure Garbage Collection as a cron job, integrate with a CI/CD pipeline that pushes on every Git tag, create Kubernetes pull secrets, and set up monitoring with Prometheus metrics endpoint.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro