Set Up a Docker Registry and Version Your Images
AI generated
Docker · Registry · Image Versioning · DevOps
Set Up a Docker Registry and Version Your Images
from a private registry to a full image lifecycle

An uncontrolled flood of latest tags and anonymous Docker Hub pulls puts every production environment at risk. Teams that run a private Docker registry, adopt meaningful image versions, and define retention policies create traceable, reproducible builds and significantly reduce the risk of outages.

12 min read Registry · Tags · Semantic Versioning · Harbor · GHCR Docker Engine 26+ · Compose v2

1. Why a dedicated Docker registry makes sense

Anyone who pulls every image directly from Docker Hub accepts several hidden risks at once. Docker Hub throttles anonymous pulls to 100 requests per six hours, and in larger teams or CI environments with many parallel builds this leads to random build failures that are hard to diagnose. A private Docker registry removes this dependency entirely, because every image is stored locally and every pull happens inside your own infrastructure.

Beyond that, a dedicated Docker registry gives you full control over which images are in circulation. In regulated environments, for example those with data protection requirements or compliance rules, arbitrary base images from the internet simply cannot be used. The registry becomes the single point of trust: every image that reaches production must come from your own repository, has been reviewed, and is versioned. That makes audits significantly easier and security gaps far easier to trace.

2. Starting the Distribution Registry in five minutes

The official Distribution Registry (formerly known as Docker Registry v2) is a lean Go program available as a Docker image on Docker Hub. It needs no database, stores all image layers as plain files, and is therefore trivial to operate. For internal development environments, a single docker compose up command is enough to start a working Docker registry that is immediately ready for push and pull operations.

By default, the registry listens on port 5000. To push an image to it, the image name must include the registry host as a prefix: registry.local:5000/myproject/api:1.2.0. Docker distinguishes registries based on this prefix; if it is missing, Docker Hub is assumed. A local DNS entry or an entry in /etc/hosts lets you assign a readable hostname such as registry.local instead of typing an IP address every time. That saves typos and makes the configuration easier to read.


# compose.registry.yml: Local Docker Registry with persistent storage
services:
  registry:
    image: registry:2.8
    container_name: docker-registry
    restart: unless-stopped
    ports:
      - "5000:5000"
    environment:
      # Store images in a named volume for persistence
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
      # Enable deletion API (needed for garbage collection later)
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      # Log level: debug, info, warn, error
      REGISTRY_LOG_LEVEL: info
    volumes:
      - registry-data:/var/lib/registry
      - ./registry/config.yml:/etc/docker/registry/config.yml:ro

volumes:
  registry-data:
    driver: local

# Push an image to the local registry:
# docker tag myapp:latest registry.local:5000/myapp:1.0.0
# docker push registry.local:5000/myapp:1.0.0
# List repositories via API:
# curl http://registry.local:5000/v2/_catalog

The registry API follows the OCI Distribution Specification and is fully documented. A simple curl call lists all existing repositories and queries the available tags for an image. That is useful for monitoring scripts that check whether a given image is present in the expected version before a deployment starts.

3. Securing it with TLS and Basic Auth

Docker only accepts a Docker registry without TLS if it is explicitly configured as an "insecure registry". That is acceptable for laptops and local testing, but a serious problem for shared infrastructure, since credentials and image layers are transmitted in plain text. TLS can be configured directly in the registry or, more flexibly, provided by an upstream reverse proxy such as Nginx or Traefik. This approach has the advantage that the registry itself stays simple to configure while the proxy handles TLS termination for multiple services.

Basic Auth for the registry is also most conveniently configured in the proxy. Docker understands the htpasswd format natively: htpasswd -Bc /auth/htpasswd username creates a user, and the file is mounted into the proxy container. For more fine-grained access control, where certain users may only read certain repositories, the official registry is somewhat limited. Harbor or the GitHub Container Registry offer considerably more options here. For teams of five or more, switching to a full registry platform is usually more worthwhile than hand-configuring authorization plugins.


# nginx-registry.conf: TLS termination and Basic Auth for Docker Registry
# Place in /etc/nginx/conf.d/registry.conf

server {
    listen 443 ssl http2;
    server_name registry.example.com;

    # TLS certificates (use mkcert for local dev or Let's Encrypt for production)
    ssl_certificate     /etc/nginx/certs/registry.crt;
    ssl_certificate_key /etc/nginx/certs/registry.key;

    # Increase max body size, Docker layers can be several hundred MB
    client_max_body_size 2000m;

    # Basic Auth protection
    auth_basic           "Docker Registry";
    auth_basic_user_file /etc/nginx/auth/htpasswd;

    location / {
        # Proxy to the registry container (must be on same Docker network)
        proxy_pass          http://registry:5000;
        proxy_set_header    Host              $host;
        proxy_set_header    X-Real-IP         $remote_addr;
        proxy_set_header    X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Proto $scheme;

        # Chunked transfer for large uploads
        proxy_request_buffering off;
        proxy_read_timeout      600;
    }
}

4. Meaningful tags and semantic versioning

The latest tag is an anti-pattern in production. It says nothing about when the image was built, which code state it contains, or whether it has changed since the last deployment. A deployment using latest is not reproducible, because a later pull can deliver a different version than the original one. The solution is consistent image versioning using semantic versioning: 1.2.3 for stable releases, 1.2.3-rc.1 for release candidates, and 1.2.3-dev.abc1234 for feature branches.

Alongside semantic version tags, immutable content hash tags based on the Git commit are recommended: sha-abc1234. This tag never changes and identifies exactly which commit an image contains. In CI pipelines, both are typically built at the same time: a readable version tag and a Git SHA tag. The version tag points to the latest image of a given version, while the SHA tag makes an image permanently traceable to its origin. Kubernetes and other orchestrators should always use SHA tags to prevent accidental overwrites during rollouts.

5. Tagging and pushing images in the CI pipeline

In modern CI/CD systems such as GitHub Actions, GitLab CI, or Drone, building and pushing Docker images is a standard operation. The challenge lies in consistent tagging: an image should carry both a readable version tag and the Git SHA. Docker lets you assign any number of tags to an image; all of them point to the same layer stack and cause no additional storage use in the registry. Tagging in the CI pipeline therefore costs nothing beyond a few milliseconds for the push operations.

One important practice is multi-platform building with docker buildx. Teams that need images for both linux/amd64 and linux/arm64, for example because developers work on Apple Silicon Macs while production runs on x86 servers, build both variants in a single pipeline and push them as a multi-arch manifest. The pulling client then automatically receives the image matching its architecture, with no need to distinguish this in the image name.


#!/usr/bin/env bash
# ci-build-push.sh: Build, tag and push Docker image in CI
set -euo pipefail

# Read version from git or environment variable
GIT_SHA="$(git rev-parse --short HEAD)"
GIT_TAG="${CI_TAG:-}"
REGISTRY="${REGISTRY:-registry.example.com}"
IMAGE_NAME="${IMAGE_NAME:-myapp}"

# Determine version tag
if [[ -n "$GIT_TAG" ]]; then
  VERSION_TAG="$GIT_TAG"
else
  # Feature branch: use branch name + sha
  BRANCH="${CI_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}"
  SAFE_BRANCH="${BRANCH//\//-}"
  VERSION_TAG="${SAFE_BRANCH}-${GIT_SHA}"
fi

FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}"

# Build multi-platform image with buildx
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag "${FULL_IMAGE}:${VERSION_TAG}" \
  --tag "${FULL_IMAGE}:sha-${GIT_SHA}" \
  --cache-from "type=registry,ref=${FULL_IMAGE}:buildcache" \
  --cache-to   "type=registry,ref=${FULL_IMAGE}:buildcache,mode=max" \
  --push \
  .

echo "Pushed: ${FULL_IMAGE}:${VERSION_TAG}"
echo "Pushed: ${FULL_IMAGE}:sha-${GIT_SHA}"

6. Image retention and garbage collection

Without active management, a Docker registry grows without bound. Every build adds new layers, old tags are rarely deleted manually, and after a year the registry can occupy several hundred gigabytes. Image retention policies define how many versions of an image are kept and when old tags may be deleted. The Distribution Registry itself offers no built-in retention policies; deleting tags must be handled through the API or external tools such as docker-registry-pruner.

Important: in the Distribution Registry, deleted tags are initially removed only as manifest entries; the actual layer data (blobs) remains on disk. Only garbage collection, registry garbage-collect /etc/docker/registry/config.yml, removes blobs that are no longer referenced and frees up space. This process ideally runs at night as a cron job and should be executed in read-only mode while running, to preserve data consistency. Harbor automates these steps completely and offers a web UI for configuring retention rules.

7. Harbor as an enterprise alternative

Harbor is a CNCF graduated open source registry platform that goes far beyond the features of the Distribution Registry. It offers role-based access control at the project level, integrated vulnerability scanning with Trivy or Clair, image signing with Notary, automatic retention policies, and a complete web interface. For teams managing multiple projects with different access permissions, Harbor is the most compelling self-hosted option.

Running Harbor requires more infrastructure than the plain Distribution Registry: Harbor needs a PostgreSQL database, a Redis cache, and several of its own microservices. The official harbor-installer distribution ships a compose setup that bundles all components together. For production environments, a Kubernetes deployment via a Helm chart is recommended, enabling rolling updates without downtime. Vulnerability scanning runs asynchronously after every push and blocks deployments of images with critical CVEs when the corresponding policy is active.

8. GitHub Container Registry as a lightweight option

The GitHub Container Registry (GHCR) is the most obvious private Docker registry alternative, without any infrastructure overhead, for teams that already use GitHub for source control and CI. Images are stored under ghcr.io/organization/imagename:tag and share the access permissions of the GitHub repository. A repository maintainer can push images automatically, while external contributors get pull access only, with no extra configuration.

GHCR supports OCI artifacts, which means not only Docker images but also Helm charts, WASM modules, and other artifacts can be stored in the same registry. Public packages are free for open source projects, and storage for private packages is included in the GitHub plan. The only relevant downside compared to a self-hosted registry is the dependency on GitHub's infrastructure: during a GitHub outage, your own images become unreachable too.

9. Registries compared

Choosing the right Docker registry depends on team size, compliance requirements, and existing infrastructure. There is no universally best solution; every option has clear strengths and limitations.

Registry Hosting Access Control Standout Feature
Distribution Registry Self-hosted Basic Auth via proxy Minimal, no UI, no scanning
Harbor Self-hosted RBAC, OIDC, LDAP Scanning, signing, retention UI
GHCR GitHub-managed GitHub permissions OCI artifacts, zero infrastructure overhead
AWS ECR AWS-managed IAM policies Native ECS/EKS integration, lifecycle policies
Docker Hub Docker-managed Teams, organizations Pull rate limit for anonymous requests

For solo developers and small teams without compliance requirements, GHCR is the simplest choice, as long as GitHub is already in use. Mid-sized teams with their own server and a desire for full control turn to Harbor. Anyone already invested in AWS infrastructure should favor ECR for its native IAM integration. The Distribution Registry works mainly as a cache or for fully airgapped environments with no internet access.

Mironsoft

Docker infrastructure, registry setup, and CI/CD integration

Want your own Docker registry with clean image versioning?

We set up private registries, define tagging strategies, integrate vulnerability scanning, and automate the entire image lifecycle in your CI/CD pipeline.

Registry Setup

Setting up and hardening Distribution Registry, Harbor, or GHCR, whichever fits your needs

Tagging Strategy

Consistently integrating semantic versioning and Git SHA tags into every CI pipeline

Lifecycle Management

Automating retention policies, garbage collection, and monitoring for the registry

10. Summary

A dedicated Docker registry solves three core problems at once: Docker Hub rate limits disappear, all images are internally versioned and traceable, and compliance requirements around data storage can be met. The Distribution Registry is the fastest way to get started, Harbor offers enterprise features for larger teams, and GHCR is the smoothest option for GitHub users. Which platform you choose is secondary; what matters more is consistently adopting a tagging strategy.

Semantic version tags combined with Git SHA tags produce a fully traceable image history. Every deployment can be traced back to a specific Git commit. Garbage collection and retention policies prevent uncontrolled registry growth. With these three building blocks, a sound registry, clear tags, and active lifecycle management, the Docker registry stops being a bottleneck and becomes a reliable foundation of the deployment pipeline.

Docker Registry and Image Versioning: The Essentials at a Glance

Choosing a Registry

Distribution Registry for minimal setups, Harbor for RBAC and scanning, GHCR for GitHub teams, ECR for AWS-native stacks.

Tagging Strategy

No latest in production. Semantic versioning (1.2.3) combined with Git SHA tags (sha-abc1234) for full traceability.

Hardening

TLS via reverse proxy, Basic Auth, or OIDC. Never expose a registry without authentication on the network.

Lifecycle

Retention policies and garbage collection as a cron job. Without active management, the registry grows uncontrolled to several hundred GB.

11. FAQ: Setting Up a Docker Registry and Versioning Images

1Why avoid the latest tag in production?
latest does not indicate a code state and can deliver a different version on a later pull. Semantic versioning and Git SHA tags make the image state unambiguous and deployments reproducible.
2What does a private Docker registry cost?
Distribution Registry is free and open source, only server and storage cost money. GHCR is included in the plan for GitHub users. Harbor is also open source but needs more resources.
3Can I put multiple tags on the same image?
Yes, without extra storage use. Multiple tags can point to the same image digest, 1.2.3, latest-stable, and sha-abc1234 can all be active at the same time.
4How do I delete old images?
Delete the manifest via the registry API, then run registry garbage-collect. Storage delete mode must be enabled in the registry configuration.
5Docker Hub versus a private registry?
Docker Hub has rate limits for anonymous pulls. A private registry has no rate limits, lives in your own infrastructure, and is not subject to external data protection concerns.
6When is Harbor worth it?
At around five developers or more, or with compliance requirements. Harbor offers RBAC, scanning, signing, and automatic retention policies, well beyond the Distribution Registry.
7Is TLS mandatory for the registry?
Yes, for any registry accessed by more than one machine. Docker only accepts HTTP registries when explicitly configured as insecure registries, acceptable only for purely local setups.
8How do multi-arch images work?
docker buildx build with --platform linux/amd64,linux/arm64 pushes an OCI manifest list. The client automatically receives the variant matching its architecture.
9What is garbage collection?
Deleting a tag leaves the layer data on disk. registry garbage-collect removes blobs that are no longer referenced and frees up space, ideally run as a nightly cron job.
10Integrating a private registry with GitHub Actions?
docker/login-action with registry credentials stored as GitHub Secrets. For GHCR, the automatic GITHUB_TOKEN is enough. After that, docker build and docker push work directly against the private registry.