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.
Table of Contents
- 1. Why a dedicated Docker registry makes sense
- 2. Starting the Distribution Registry in five minutes
- 3. Securing it with TLS and Basic Auth
- 4. Meaningful tags and semantic versioning
- 5. Tagging and pushing images in the CI pipeline
- 6. Image retention and garbage collection
- 7. Harbor as an enterprise alternative
- 8. GitHub Container Registry as a lightweight option
- 9. Registries compared
- 10. Summary
- 11. FAQ
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.