Registry #
The Docker Registry is the distribution heart of the Docker ecosystem. Without a registry, images you build on your laptop would never reach production servers, CI pipelines would have no consistent artifact, and team collaboration would stall at “please send me your Docker file by email”. The registry answers one simple question: where are images stored, and how are they distributed to every host that needs them?
This article covers Docker Registries inside and out. We’ll dissect their anatomy (repository, tag, manifest, layer), compare public and private registries, look at the push-pull workflow in detail, and finally discuss the security aspects that are often overlooked: image scanning, supply chain attacks, and safe tagging strategies. After reading this article, you’ll be able to choose the right registry for your organization and design a secure image distribution pipeline.
The Registry’s Role in the Docker Ecosystem #
The registry is the third layer in Docker’s architecture — where images are stored and distributed. Without a registry, the only way to share an image is docker save (exporting to a file) and copying that file to another host. That isn’t scalable, auditable, or secure.
flowchart LR
DEV[Developer]
CI[CI/CD Pipeline]
subgraph REG["Registry"]
R1[Repository: api]
R2[Repository: web]
R3[Repository: worker]
end
STAGING[Staging Server]
PROD_K8S["K8s Cluster<br/>(production)"]
DEV2[Developer 2]
DEV -->|"docker push<br/>api:v1.0"| R1
CI -->|"docker push<br/>api:abc123"| R1
R1 -->|"docker pull"| STAGING
R1 -->|"docker pull"| PROD_K8S
R1 -->|"docker pull<br/>(debug)"| DEV2The registry is the single source of truth for every image in the organization. An image pushed to the registry can be pulled by anyone with access — from a developer’s laptop to Kubernetes nodes in three different regions. That’s what makes deployments reproducible and scalable.
Why the Registry Matters #
WITHOUT a registry:
✗ Images only exist locally on one machine.
✗ Deployment requires copying files between hosts (manual, error-prone).
✗ No version control for images.
✗ No safe way to share images between teams.
✗ No audit trail (who pushed what, when).
WITH a registry:
✓ Images stored centrally, backed up, replicated.
✓ Deployment is just pull by tag.
✓ Image versioning via tags and digests.
✓ Access control per repository, per user, per team.
✓ Complete audit logs (who pushed/pulled what, when).
✓ Image scanning for CVEs.
✓ CDN-like distribution (images cached at the edge).
A way to remember: think of the registry as the “GitHub for Docker images”. Just as GitHub stores code, the registry stores images. Just as you git clone code, you docker pull images. Just as GitHub has public and private repos, the registry has the same concept.
Registry Anatomy: Repository, Tag, Manifest, Layer #
The registry isn’t just a file server for images. It’s a service managing four core concepts: repository, tag, manifest, and layer. All four are interrelated and form a neat hierarchy.
flowchart TB
REG["Docker Registry"]
REG --> REPO_A["Repository: myapp"]
REG --> REPO_B["Repository: nginx"]
REG --> REPO_C["Repository: postgres"]
REPO_A --> TAG_A1["Tag: latest<br/>(mutable)"]
REPO_A --> TAG_A2["Tag: v1.0.0<br/>(immutable)"]
REPO_A --> TAG_A3["Tag: v1.1.0<br/>(immutable)"]
TAG_A2 --> MANIFEST["Image Manifest<br/>(sha256:aaa...)"]
MANIFEST --> CONFIG["Image Config<br/>(CMD, ENV, etc.)"]
MANIFEST --> L1["Layer 1<br/>(sha256:111)"]
MANIFEST --> L2["Layer 2<br/>(sha256:222)"]
MANIFEST --> L3["Layer 3<br/>(sha256:333)"]Repository #
A repository is a collection of related images. Usually one repository = one application or one service.
Repository name format:
[NAMESPACE/]REPOSITORY
Examples:
library/nginx # official image, namespace "library" implicit
unisbadri/myapi # user "unisbadri", repo "myapi"
myorg/frontend/web # org "myorg", sub-namespace "frontend", repo "web"
A repository is the unit of access control. You can grant push permission on a specific repository to one user or team without giving access to other repositories.
Tag #
A tag is an identifier inside a repository. It points to a specific digest of the image manifest.
Full reference format:
[REGISTRY[:PORT]/][NAMESPACE/]REPOSITORY[:TAG][@DIGEST]
Examples:
docker.io/library/nginx:1.25.3
ghcr.io/unisbadri/myapi:v2.1.0
123456789.dkr.ecr.us-east-1.amazonaws.com/prod-api:2026-02-07
myregistry.local:5000/internal/worker@sha256:b5b2b2c5...
Tags are mutable, digests are immutable. A tag is a pointer that can be moved to another manifest (for example on a re-push). A digest is a content-addressable identifier that never changes for the same image.
# Mutable tag: a second push overwrites the pointer
docker push myapp:v1.0 # digest A
docker push myapp:v1.0 # digest B (v1.0 now points to B)
# Immutable digest: pulling by digest always gets the same image
docker pull myapp@sha256:aaa... # always digest A
Anti-pattern: using thelatesttag in production.latestis the default tag that automatically resolves to the NEWEST digest. If you re-push an image with thelatesttag, a running container (or a deployment script usinglatest) will pull the NEW image you haven’t tested. Always use explicit tags (semver, date, commit SHA) or digests for deployments.
Manifest #
A manifest is the JSON describing an image — the list of layers, configuration, and architecture. Manifests are stored in the registry with a content-addressable digest.
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:b5b2b2c507a0944348e0303112d8d08cba2bd51a7f..."
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:e692418e4d3c4f7d3e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"size": 77956920
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:3e3b1767b39736f5c0e3b4d5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5",
"size": 12345678
}
]
}
Important fields:
config.digest— pointer to the image configuration JSON.layers[]— the list of layers, each with a digest and size.- Content-addressable — the digest is the
sha256of the content, so the registry can verify integrity.
Layer #
A layer is an immutable filesystem unit. Each Dockerfile instruction produces one layer. Layers are stored in the registry and cached on hosts that pull them.
flowchart LR
BASE["Base image<br/>ubuntu:22.04<br/>77 MB"]
PKG["apt install nginx<br/>12 MB"]
CONF["COPY config<br/>0.1 MB"]
BASE --> PKG --> CONF
CACHE[Host Local Cache]
BASE -.-> CACHE
PKG -.-> CACHE
CONF -.-> CACHEThe benefits of layer sharing:
- Incremental pulls — if you already have
ubuntu:22.04on the host, pulling a new image using the same base won’t re-download the base layer. - Efficient storage — the registry stores one copy of a layer shared by many images.
- Build cache — when building a new image, the same layers are reused.
Interesting detail: layer sharing happens across repositories, tags, and images. For example, python:3.12-slim and node:20-alpine probably don’t share layers (different bases), but myapp:v1.0 and myapp:v2.0 most likely share many layers (base image, dependencies). That’s why upgrading from v1 to v2 doesn’t always mean “re-download everything”.
Types of Docker Registries #
Registries fall into three broad categories: public registries (anyone can access), private cloud registries (managed by a cloud provider), and self-hosted registries (you run them yourself). Each has different characteristics, advantages, and trade-offs.
Public Registries #
| Registry | Provider | Use Case |
|---|---|---|
| Docker Hub | Docker Inc. | Default, official images, community |
| GitHub Container Registry (GHCR) | GitHub | GitHub Actions integration |
| Quay.io | Red Hat | Image security, Clair scanning |
| AWS Public ECR Gallery | AWS | Public images from AWS services |
Docker Hub is the largest public registry with 100+ billion pulls per year. “Official” images (nginx, redis, postgres, etc.) live in the library/ namespace and are audited by Docker. Community images can be pulled by anyone without authentication.
Advantages:
- Easy to use, the default in the
dockerCLI. - Official images are quality-assured (regular audits, security updates).
- Free for public repos.
Drawbacks:
- Rate limits for anonymous pulls (100 pulls / 6 hours per IP) and authenticated pulls (200 pulls / 6 hours per user).
- Supply chain risk: unofficial images could be malicious or backdoored.
- No SLA for availability (usually high, but not guaranteed).
Private Cloud Registries #
| Provider | Service | Characteristics |
|---|---|---|
| AWS | Elastic Container Registry (ECR) | IAM integration, auto scan, KMS encryption |
| Google Cloud | Artifact Registry | Multi-format (Docker, Maven, npm, Python) |
| Azure | Container Registry (ACR) | AKS integration, geo-replication |
| DigitalOcean | Container Registry | Simple, clear pricing |
Private cloud registries are registries you control (you hold the access) but are managed by the cloud provider. Their main advantages:
- IAM integration — access control via AWS/GCP/Azure roles, no separate user management.
- Network proximity — the registry sits in the same region as your cluster, faster pulls.
- Compliance — the cloud provider’s compliance certifications (SOC2, HIPAA, etc.) apply.
- Zero operational overhead — no need to install, patch, back up, or scale the registry yourself.
Trade-offs:
- Vendor lock-in — even though the Docker Registry Protocol is standard, proprietary features (like IAM integration) aren’t portable.
- Cost — pulls from other regions or over the internet usually incur egress charges.
- Quotas — some providers limit the number of repos or total size.
Self-Hosted Registries #
Self-hosted registries fit:
- Very strict compliance needs (on-premise data centers, nothing may leave).
- Full isolation from the internet.
- Very low latency between local data centers.
- Lower long-term costs at high volume.
| Tool | Characteristics |
|---|---|
| Docker Registry (official) | registry:2 image, minimal, lightweight |
| Harbor | Enterprise-grade, UI, scanning, replication, RBAC |
| JFrog Artifactory | Universal artifact repo (not just containers) |
| Sonatype Nexus | Universal artifact repo, supports many formats |
| GitLab Container Registry | Integrated with GitLab CI/CD |
flowchart TB
subgraph DOCKER["Docker Registry (official)"]
A1[Minimal]
A2[REST API only]
A3[No UI]
end
subgraph HARBOR["Harbor"]
B1[Web UI]
B2[RBAC]
B3[Vulnerability scan]
B4[Replication]
B5[Image signing]
end
subgraph ARTIFACTORY["JFrog Artifactory"]
C1[Universal artifact]
C2[Build info]
C3[Enterprise features]
endDocker Registry (registry:2) is the minimal implementation — REST API only, no UI, no scanning. Good for small setups or as a building block for larger solutions. Harbor is the most popular enterprise choice: full UI, granular RBAC, image scanning (Trivy/Clair), cross-instance replication, and image signing (Cosign/Notary).
Recommendation: for most teams, start with a cloud provider registry (ECR, GAR, ACR) closest to their infrastructure. Move to self-hosted (Harbor) only when there’s a specific need cloud registries can’t meet: on-premise mandates, special compliance, or volume making cloud registries expensive.
Push and Pull Workflows #
Let’s look in detail at what happens when you docker push and docker pull. This understanding helps debug the push/pull issues that often show up in production.
Push Workflow #
sequenceDiagram
participant CLI as docker CLI
participant D as dockerd
participant REG as Registry
CLI->>D: docker push myapp:v1.0
D->>D: Resolve image ID & layer digests
D->>REG: HEAD /v2/myapp/manifests/v1.0
REG-->>D: 404 (manifest doesn't exist)
D->>REG: POST /v2/myapp/blobs/uploads/ (start upload)
REG-->>D: 202 Accepted (upload UUID)
D->>REG: PATCH /v2/myapp/blobs/uploads/<uuid> (layer 1)
REG-->>D: 200 OK
D->>REG: PATCH /v2/myapp/blobs/uploads/<uuid> (layer 2)
REG-->>D: 200 OK
D->>REG: PUT /v2/myapp/blobs/uploads/<uuid>?digest=sha256:... (finalize)
REG-->>D: 201 Created
D->>REG: PUT /v2/myapp/manifests/v1.0 (push manifest)
REG-->>D: 201 Created
D-->>CLI: push completeThe key steps:
- Resolve image & layers — the daemon reads the image ID, then the list of layers that compose it.
- Check for an existing manifest —
HEADagainst the registry to see if the manifest already exists. Mounting trick: for layers that already exist, the daemon canPOST .../?mount=<digest>to “mount” a layer from another blob without re-uploading. - Upload layers one by one — layers missing from the registry are uploaded via chunked upload (PATCH per chunk).
- Push the manifest — once all layers exist, the manifest is pushed and points at those layers.
Pull Workflow #
sequenceDiagram
participant CLI as docker CLI
participant D as dockerd
participant REG as Registry
CLI->>D: docker pull myapp:v1.0
D->>REG: GET /v2/myapp/manifests/v1.0
REG-->>D: manifest (config + layers)
D->>D: Compare with local cache
D->>REG: HEAD /v2/myapp/blobs/<layer1-digest>
REG-->>D: 200 OK (exists)
D->>REG: GET /v2/myapp/blobs/<layer1-digest>
REG-->>D: layer 1 data
D->>REG: HEAD /v2/myapp/blobs/<layer2-digest>
REG-->>D: 200 OK
D->>REG: GET /v2/myapp/blobs/<layer2-digest>
REG-->>D: layer 2 data
D->>D: Apply layers (extract, snapshot)
D-->>CLI: pull completeOptimizations That Happen Frequently #
Layer deduplication and mounting: on push, if a layer with the same digest already exists in the registry (e.g. from another image), the daemon doesn’t re-upload it — it just “mounts” a reference to the existing blob. That’s why pushing a second image with the same base is far faster than the first push.
Parallel downloads: modern pulls (Docker 18.09+) download multiple layers in parallel. An image with many small layers pulls faster than one with a single large layer (relevant for image design).
Resumable uploads: chunked uploads (PATCH) can be resumed if the connection drops. This is a big help for pushing large images over unstable networks.
Host-side caching: after the first pull, layers are cached in /var/lib/docker/overlay2/. Subsequent pulls of images sharing layers won’t re-download them.
A common production problem: Docker Hub rate limits. When CI/CD pulls many different images in a short time, anonymous CI can hit the 100-pull/6-hour rate limit. Solutions: mirror frequently used images to an internal registry, or use authenticated Docker Hub pulls with an organization account (200 pulls/6 hours per user).
Authentication and Authorization #
Modern registries support several authentication mechanisms, from the simplest to the most sophisticated.
Authentication Types #
| Type | How It Works | Use Case |
|---|---|---|
| Anonymous | No credentials needed, public registry | Docker Hub public images |
| Basic Auth | Username + password (HTTP Basic) | Simple, self-hosted registries |
| Bearer Token | JWT issued after login | Docker Hub, GHCR, cloud registries |
| OAuth 2.0 | Standard OAuth flow, IdP integration | Enterprise, GitHub Apps |
| IAM-based | Cloud IAM (AWS IAM, GCP Service Account) | ECR, GAR, ACR |
| mTLS | Mutual TLS with client certificates | Zero-trust networks |
Logging Into a Registry #
# Log in to Docker Hub
docker login
# Username: your-username
# Password: ************
# Log in to a custom registry
docker login registry.example.com
# Log in with IAM (AWS ECR)
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 123.dkr.ecr.us-east-1.amazonaws.com
# Log out
docker logout registry.example.com
Credentials are stored in ~/.docker/config.json. For CI/CD environments, it’s safer to use short-lived tokens or a secret manager than hardcoded passwords.
Authorization — Access Control #
Authorization is who is allowed to do what — usually more complex than authentication.
| Action | Example |
|---|---|
| Pull | Who may pull images from repo X? |
| Push | Who may push images to repo Y? |
| Delete | Who may delete images or tags? |
| List | Who may view the image list in a registry? |
Authorization implementations differ per registry:
- Docker Hub: simple roles (admin, contributor, reader) per repo.
- AWS ECR: IAM policies with
Action: ecr:GetDownloadUrlForLayeretc. - Harbor: granular roles (project admin, developer, guest) per project.
- GHCR: package visibility (public, private, internal) + GitHub team permissions.
Least privilege principle: for CI/CD, create a dedicated service account that only has push permission to the registry, and rotate its token regularly. Don’t use personal developer accounts to push from CI — if a token leaks, a service account scoped to specific repos has a smaller blast radius.
Image Scanning and Supply Chain Security #
A registry isn’t just a place to store images — it’s also an important control point for security. Pushed images can contain vulnerabilities (CVEs in packages) or even malicious code (backdoors, crypto miners). Image scanning detects these before images reach production.
Image Scanning Anatomy #
flowchart LR
A[Image pushed] --> B[Scanner]
B --> C{Scan type}
C -- Vulnerability --> D[Trivy, Snyk, Clair]
C -- Compliance --> E[Config scan, CIS benchmark]
C -- Secrets --> F[GitLeaks, TruffleHog]
D --> G[Report CVEs]
E --> H[Report config issues]
F --> I[Report leaked secrets]
G --> J{Gate}
H --> J
I --> J
J -- Critical --> K[Block deploy]
J -- High --> L[Manual review]
J -- Medium/Low --> M[Deploy with monitoring]Popular Scanning Tools #
| Tool | Type | License | Integration |
|---|---|---|---|
| Trivy | Vulnerability, misconfig, secrets | Open source (Apache 2.0) | CI/CD, registry, runtime |
| Snyk | Vulnerability, license | Commercial + free tier | GitHub, GitLab, registry |
| Clair | Vulnerability (static analysis) | Open source (Apache 2.0) | Quay, Harbor |
| Docker Scout | Vulnerability, recommendations | Commercial (Docker subscription) | Docker Hub, Docker Desktop |
| AWS ECR Scan | Vulnerability | Included with ECR | AWS ecosystem |
| GCP Container Analysis | Vulnerability | Included with GAR | GCP ecosystem |
| Anchore | Vulnerability, policy | Open source + commercial | Jenkins, K8s |
Supply Chain Attacks — a Real Risk #
A supply chain attack is one where attackers infiltrate a dependency or tool you use, so malicious code reaches the images you build. Some real examples:
- SolarWinds (2020) — malicious code in software updates used by many companies.
- CodeCov (2021) — credentials leaked from a CI tool; attackers modified the scripts being pulled.
- event-stream npm (2018) — the original maintainer handed the package to an attacker; malicious code reached thousands of projects.
- XZ Utils backdoor (2024) — malicious code in a compression library, nearly reaching many Linux distributions.
Layered defense for supply chain security:
flowchart TB
A[Base image] --> B{Pinned to digest?}
B -- Yes --> C[Verify integrity]
B -- No --> D[Auto-update could break]
C --> E[Build image]
E --> F{Scan?}
F -- Yes --> G[Detect CVEs]
F -- No --> H[Blind deploy]
G --> I{Sign image?}
I -- Yes --> J[Verify signature]
I -- No --> K[Anyone could replace]
J --> L[Deploy]- Pin base images to digests —
FROM ubuntu@sha256:abc...instead ofFROM ubuntu:22.04. This prevents mutable tags. - Verify image signatures — use Cosign (part of Sigstore) or Docker Content Trust (DCT) to sign images.
- Scan at every stage — don’t just scan the final image; scan the base image and intermediate layers too.
- Use trusted base images —
docker.io/library/*(official) or images from trusted vendors. - Allowlisting — in K8s, use admission controllers (OPA, Kyverno) that only permit images from allowed registries.
Anti-pattern: pulling base images without verification. If an attacker compromises Docker Hub and swaps the library/nginx image for a backdoored one, every build that doesn’t pin to a digest automatically becomes compromised. Always pin base images to SHA256 digests and verify their signatures.Self-Hosted Docker Registry — a Practical Setup #
For a simple setup, the official Docker Registry (registry:2) is enough. Here’s an example docker-compose.yml for a self-hosted registry with TLS and basic auth.
# docker-compose.yml
version: "3.9"
services:
registry:
image: registry:2
restart: always
ports:
- "5000:5000"
environment:
REGISTRY_HTTP_SECRET: a-very-long-random-string
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
volumes:
- ./auth:/auth
- registry-data:/var/lib/registry
deploy:
resources:
limits:
memory: 512M
volumes:
registry-data:
# Create an admin user
mkdir -p auth
docker run --rm \
--entrypoint htpasswd \
httpd:2.4 -bn admin secretpassword > auth/htpasswd
# Run it
docker compose up -d
# Log in and push
docker login registry.local:5000
docker tag myapp:v1.0 registry.local:5000/myapp:v1.0
docker push registry.local:5000/myapp:v1.0
For an enterprise setup, Harbor is a more solid choice. Harbor adds:
- A web UI for browsing images and managing users.
- Image scanning (built-in Trivy/Clair).
- Replication between Harbor instances (multi-region).
- Image signing and verification (Cosign, Notary).
- Granular per-project RBAC.
Registry Best Practices for Production #
Safe Tagging Strategies #
# DON'T in production
docker push myapp:latest # mutable, not reproducible
# DO in production
docker push myapp:v1.4.2 # semantic version
docker push myapp:v1.4.2-alpine # base variant
docker push myapp:1.4.2-build.123 # build number
docker push myapp:2026-02-07-abc1234 # date + short SHA
# BEST for reproducibility
docker push myapp@sha256:b5b2b2c5... # full digest
Separate Registries per Environment #
flowchart TB
DEV[Development] --> R1[registry.dev.internal]
STAGING[Staging] --> R2[registry.staging.internal]
PROD[Production] --> R3[registry.prod.internal]
CI[CI/CD Pipeline] --> R1
CI --> R2
CI --> R3Separating registries per environment prevents untested images from leaking into production and makes auditing easier (who pushed to the production registry).
Enable Image Scanning #
Make sure every image entering the registry is scanned, and deployment is blocked for images with critical CVEs.
Retention Policies #
A registry that’s never cleaned will bloat. Implement a retention policy:
- Keep the last N tags per repository.
- Keep tags from the last X days.
- Delete untagged images older than Y days.
- Archive old images to object storage (S3, GCS) before deletion.
Clean Up Old Images #
# Remove images with no tags that are more than 30 days old
docker exec registry /bin/registry garbage-collect \
-m /etc/docker/registry/config.yml
# Or in Harbor: Settings > Tag Retention > create an automatic policy
General best practice: set retention policies per environment — dev/staging can be more aggressive (delete after 7 days), production more conservative (keep 90 days or per compliance). For disaster recovery, always back up the production registry to a second region.
Decision Tree — Choosing the Right Registry #
flowchart TD
A{Deployment<br/>environment?}
A -- Public internet --> B{Budget?}
B -- Minimal --> C[Docker Hub free]
B -- Available --> D[Docker Hub Pro / Team]
A -- Cloud AWS --> E[ECR]
A -- Cloud GCP --> F[Artifact Registry]
A -- Cloud Azure --> G[Container Registry]
A -- On-premise --> H{Need enterprise<br/>features?}
H -- Yes --> I[Harbor]
H -- Minimal --> J[Docker Registry 2.x]
A -- Hybrid --> K[Harbor replicated<br/>with cloud registry]
E --> L[Enable scan, KMS]
F --> M[Enable scan, CMEK]
G --> N[Enable scan, geo-replication]
I --> O[Set up RBAC, scan, signing]
J --> P[TLS, basic auth, backup]Choosing a registry isn’t a one-time decision. Many organizations start with a cloud registry (ECR/GAR) for setup speed, then add Harbor for specific images needing more control (e.g. proprietary images with sensitive IP). Hybrid patterns like this are common in large companies.
Summary #
- The Docker Registry is the single source of truth for images in an organization. It answers “where are images stored” and “how are images distributed to every host” — without it, images can only be shared manually via files.
- The four core registry concepts: repository (a collection of related images), tag (a mutable label for identification), manifest (the JSON describing an image), layer (an immutable filesystem unit). Tags are mutable, digests immutable — for reproducibility, always pull by digest.
- Three registry types: public (Docker Hub, GHCR) for open-source and community images; private cloud (ECR, GAR, ACR) for proprietary images with IAM integration; self-hosted (Harbor) for full control and strict compliance.
- The push workflow uploads layers one by one (chunked), with a mounting trick for layers that already exist. The pull workflow downloads the manifest first, then only the layers missing from the cache. Layer sharing makes pulls incremental and storage efficient.
- Image scanning is a mandatory gate in production pipelines. Trivy, Snyk, and Docker Scout detect CVEs in packages. Supply chain security requires layered defense: pin base images to digests, sign images (Cosign), scan at every stage, and allowlist images in the K8s admission controller.
- Safe tag strategy: use semantic versions, commit SHAs, or digests. NEVER use
latestin production. Separate registries per environment, enable image scanning, and set retention policies to prevent registry bloat.- Docker Registry 2.x is the minimal self-hosted choice. Harbor is the enterprise choice with UI, RBAC, scanning, and replication. Choose based on control needs, compliance, and operational overhead.
- Image signing (Cosign, Notary) and admission controllers (OPA, Kyverno) in K8s provide the final line of defense: only signed images from allowed registries can be deployed.