Harbor Production Architecture
Self-hosted container registry — deployment, HA, security scanning, replication & operations
Overview
Harbor is an open-source container registry that stores, signs, and scans container images. It is a CNCF Graduated project (since June 2020), which puts it in the same maturity tier as Kubernetes, Prometheus, and Envoy. The latest stable release is v2.14.x (as of early 2026), with v2.15 in release candidate. Harbor is fully OCI-compliant, meaning it works with any OCI-compatible client — Docker, Podman, Buildah, Helm, and OCI artifact tooling.
The core value proposition of Harbor is control. When you self-host a registry, you control where images live, who can access them, what security policies are enforced, and how images are distributed across environments. This matters enormously for air-gapped environments, regulated industries, multi-datacenter architectures, and organizations that need to reduce their dependency on external services.
Why self-host a registry?
- Air-gapped environments — Government, defense, financial, and healthcare systems that cannot reach the public internet need a local registry
- Compliance — Regulations like FedRAMP, HIPAA, PCI-DSS may require that container images are scanned, signed, and stored within your boundary
- Bandwidth and latency — Pulling from Docker Hub or cloud registries over the internet is slow, rate-limited, and unreliable at scale. A local registry eliminates external dependency
- Supply chain security — Vulnerability scanning, image signing (Cosign), and immutable tags give you provable guarantees about what runs in production
- Cost — Cloud registry egress charges add up fast at scale. Self-hosted is free beyond infrastructure
Strengths
- CNCF Graduated — production-proven, large community
- Built-in Trivy vulnerability scanning
- Image signing with Cosign (Sigstore); Notary v1 removed in v2.9
- Project-level RBAC with OIDC/LDAP integration
- Cross-registry replication (push & pull modes)
- Proxy cache for Docker Hub, GCR, Quay, GitHub Container Registry, etc.
- Helm chart repository support
- Fully open source — no enterprise edition, no feature gating
Considerations
- No built-in clustering — HA requires external orchestration
- PostgreSQL and Redis are hard dependencies
- Storage can grow rapidly without retention policies
- Garbage collection historically required read-only mode; non-blocking GC available since v2.1 but monitor carefully
- Upgrade path requires attention — database migrations are involved
- Web UI is functional but not as polished as commercial alternatives
Registry comparison
| Feature | Harbor | Docker Hub | ECR/GCR/ACR | Quay |
|---|---|---|---|---|
| Self-hosted | Yes (only option) | No | No (managed) | Yes + managed |
| License | Apache 2.0 | Proprietary | Proprietary | Apache 2.0 |
| Vulnerability scanning | Trivy (built-in) | Docker Scout | Varies by provider | Clair |
| Image signing | Cosign (Notary removed in v2.9) | Docker Content Trust | Provider-specific | Cosign support |
| Replication | Push + pull modes | N/A | Within provider | Geo-replication |
| Proxy cache | Yes | N/A | ECR pull-through | Yes (since Quay 3.7) |
| Air-gapped | Fully supported | No | No | Yes (self-hosted) |
| Cost | Free (infra only) | Free tier + paid | Per-storage + egress | Free + RHEL sub |
Harbor is the default choice for self-hosted container registries in the Kubernetes ecosystem. If a client needs a private registry and is not locked into a specific cloud provider, Harbor is the answer. The main open-source competitor is Quay (Red Hat), which is a capable alternative but more complex to deploy and more tightly integrated with the Red Hat ecosystem. For clients already deep in AWS/GCP/Azure, the managed registries (ECR/GCR/ACR) are simpler but sacrifice portability and air-gap capability.
Architecture
Harbor is a multi-component system. Understanding the component layout is critical for troubleshooting, capacity planning, and designing HA deployments.
Core components
| Component | Role | Notes |
|---|---|---|
| Nginx Proxy | Reverse proxy & TLS | Terminates TLS, routes requests to core, portal, or registry. All external traffic enters through Nginx. |
| Core Service | API & business logic | Authentication, authorization, project management, replication, webhook notifications. The brain of Harbor. |
| Portal | Web UI | Angular-based frontend for managing projects, images, users, and policies. Talks to Core via API. |
| Registry | Image storage (Distribution) | The CNCF Distribution (formerly Docker Distribution, registry v2) engine. Handles actual image push/pull at the blob and manifest level. |
| Job Service | Async task runner | Runs replication, garbage collection, scan jobs, retention policy execution. Queue-based via Redis. |
| PostgreSQL | Metadata database | Stores projects, users, RBAC rules, replication policies, scan results, audit logs. Critical stateful component. |
| Redis | Cache & job queue | Session cache, job queue for the Job Service, temporary data. Losing Redis is recoverable but causes a brief disruption. |
| Trivy | Vulnerability scanner | Scans images for CVEs on push or on demand. Downloads vulnerability database from GitHub (or offline DB for air-gap). |
| Notary | Image signing (removed) | TUF-based content trust. Deprecated in v2.6 and removed in v2.9. Superseded by Cosign. Not available in current Harbor versions. |
Request flow
When a client runs docker push registry.example.com/myproject/myapp:v1.2:
- Nginx receives the request, terminates TLS, and routes it
- Core Service handles authentication (checks credentials against the local DB, LDAP, or OIDC provider)
- Core Service checks RBAC — does this user/robot account have push access to the
myprojectproject? - If authorized, the request is proxied to the Registry (Distribution) component
- Registry stores image layers (blobs) in the configured storage backend (S3, filesystem, etc.)
- If scan-on-push is enabled, Core creates a scan job in Redis, which Job Service picks up and sends to Trivy
- Trivy downloads the image layers, scans for vulnerabilities, and reports results back to Core via the database
Harbor's Core Service sits in front of the standard CNCF Distribution registry and adds the features that Distribution lacks: authentication, RBAC, scanning, replication, and UI. The Registry component itself is the same distribution/distribution project (now a CNCF project) used by Docker Hub, GitHub Container Registry, and others. Harbor wraps it with enterprise features.
Deployment Models
Harbor provides an official installer that generates Docker Compose or Helm configurations. The deployment model you choose depends on your infrastructure, scale, and HA requirements.
Dev/Small Docker Compose
- Single-host deployment with all components as containers
- Built-in PostgreSQL and Redis (no external deps needed)
- Official installer script generates
docker-compose.yml - Good for dev/staging, small teams (< 50 users)
- No HA — single point of failure on every component
- Storage: local filesystem or S3-compatible backend
Production Helm Chart on K8s
- Official Helm chart deploys all components as K8s workloads
- Scales stateless components (core, portal, job service) via replicas
- External PostgreSQL and Redis recommended for production
- Ingress controller handles TLS and routing
- Persistent volumes for registry storage (or S3 backend)
- Best option for production Kubernetes environments
Legacy VM-Based
- Docker Compose on a dedicated VM (or multiple VMs)
- Common in environments without Kubernetes
- Can still achieve HA with external DB, Redis, shared storage, and a load balancer
- More operational burden than Helm-based deployment
Air-Gapped Offline Install
- Harbor installer includes offline bundle with all container images
- Trivy needs offline vulnerability database (download separately, transfer via sneakernet)
- No internet required after initial setup
- Common in government and defense deployments
Docker Compose installation
# Download the installer
wget https://github.com/goharbor/harbor/releases/download/v2.14.3/harbor-offline-installer-v2.14.3.tgz
tar xzf harbor-offline-installer-v2.14.3.tgz
cd harbor
# Copy and edit the config
cp harbor.yml.tmpl harbor.yml
# Edit harbor.yml: hostname, TLS certs, admin password, storage backend
# Run the installer
./install.sh --with-trivy
# The installer generates docker-compose.yml and starts all services
# Verify:
docker compose ps
Key harbor.yml settings
# harbor.yml (critical settings)
hostname: registry.example.com
https:
port: 443
certificate: /data/cert/server.crt
private_key: /data/cert/server.key
harbor_admin_password: ChangeMeImmediately
database:
password: db-password-here
max_idle_conns: 100
max_open_conns: 900
data_volume: /data # local storage path
storage_service:
s3:
accesskey: AKIAIOSFODNN7EXAMPLE
secretkey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region: us-east-1
bucket: harbor-registry
rootdirectory: /registry
trivy:
ignore_unfixed: false
skip_update: false # set true for air-gapped
insecure: false
Helm chart deployment
# Add the Harbor Helm repo
helm repo add harbor https://helm.goharbor.io
helm repo update
# Install with custom values
helm install harbor harbor/harbor \
--namespace harbor --create-namespace \
--set expose.type=ingress \
--set expose.ingress.hosts.core=registry.example.com \
--set expose.ingress.className=nginx \
--set externalURL=https://registry.example.com \
--set persistence.persistentVolumeClaim.registry.storageClass=gp3 \
--set persistence.persistentVolumeClaim.registry.size=500Gi \
--set database.type=external \
--set database.external.host=harbor-pg.rds.amazonaws.com \
--set database.external.port=5432 \
--set database.external.username=harbor \
--set database.external.password=secret \
--set redis.type=external \
--set redis.external.addr=harbor-redis.cache.amazonaws.com:6379 \
--values harbor-values.yaml
For production, always use external PostgreSQL and Redis. The built-in instances are single-container, no replication, no backups. Use RDS/Cloud SQL for PostgreSQL and ElastiCache/Memorystore for Redis. If on-prem, use Patroni for PostgreSQL HA and Redis Sentinel. The stateless Harbor components can be rebuilt in minutes; losing the database means losing all metadata, RBAC rules, scan results, and audit logs.
High Availability
Harbor does not have built-in clustering. HA is achieved by scaling stateless components horizontally and using HA-capable external services for stateful components. This is a common source of confusion — there is no "Harbor cluster mode" you can toggle on.
HA architecture
Stateless components (scale horizontally)
- Core Service — Run 2-3 replicas behind a load balancer. Each instance is identical. Session state is in Redis.
- Portal — Static web UI, scales trivially. Typically combined with Core in the same pod (Helm chart default).
- Registry (Distribution) — Multiple replicas all pointing at the same storage backend. S3 or shared filesystem required.
- Job Service — Runs async tasks. Multiple replicas consume from the same Redis queue. Jobs are idempotent.
- Trivy — Stateless scanner. Multiple replicas handle parallel scan requests.
Stateful components (use external HA services)
- PostgreSQL — Use managed databases (RDS, Cloud SQL, Azure DB) or Patroni for on-prem HA. Streaming replication with automatic failover.
- Redis — Use managed Redis (ElastiCache, Memorystore) or Redis Sentinel for on-prem. Redis Cluster mode is not natively supported by Harbor due to underlying library constraints — use Sentinel mode for HA.
- Storage Backend — S3 is strongly recommended for HA. It is the only storage option that is inherently distributed and fault-tolerant. Filesystem storage requires a shared NFS mount, which introduces its own SPOF unless you run an HA NFS service.
Running multiple Harbor instances with local filesystem storage and no shared backend. Each instance writes to its own disk, so images pushed to Node 1 are invisible to Node 2. You end up with a split-brain registry. Always use S3 or shared storage when running multiple Harbor instances.
Load balancer configuration
# Nginx LB example for Harbor HA
upstream harbor_backend {
server harbor-node1:443;
server harbor-node2:443;
server harbor-node3:443;
}
server {
listen 443 ssl;
server_name registry.example.com;
ssl_certificate /etc/ssl/certs/registry.crt;
ssl_certificate_key /etc/ssl/private/registry.key;
# Large body size for image pushes
client_max_body_size 0;
# Chunked transfer encoding for large layers
chunked_transfer_encoding on;
location / {
proxy_pass https://harbor_backend;
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;
proxy_buffering off;
proxy_request_buffering off;
}
}
Image layer uploads can be multi-gigabyte. Ensure your load balancer has client_max_body_size 0 (unlimited) and reasonable timeouts. Proxy buffering should be disabled for streaming uploads. A 30-second timeout will kill large layer pushes.
Storage Backends
Harbor's registry component uses the Docker Distribution storage driver system. The storage backend holds image layers (blobs) and manifests. Choosing the right backend is one of the most impactful decisions for a Harbor deployment.
Recommended S3-Compatible
- AWS S3, MinIO, Ceph RADOS Gateway, Wasabi
- Inherently distributed, fault-tolerant, scalable
- Best option for HA and multi-instance deployments
- Supports lifecycle policies for cost optimization
- Egress costs can add up with frequent pulls
Cloud GCS / Azure Blob
- Native storage driver support in Distribution
- Same benefits as S3 within respective cloud
- Tight IAM integration (workload identity, managed identity)
- Good choice when Harbor runs in the same cloud
Simple Filesystem
- Local disk or NFS mount
- Simplest to set up, no external dependencies
- Cannot be shared across instances without NFS
- NFS introduces latency and single point of failure
- Suitable for single-node or dev deployments
Legacy Swift
- OpenStack Swift object storage
- Relevant for OpenStack-based private clouds
- Less common in modern deployments
- Full storage driver support in Distribution
S3 storage configuration
# harbor.yml storage configuration for S3
storage_service:
s3:
accesskey: AKIAIOSFODNN7EXAMPLE
secretkey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region: us-east-1
bucket: harbor-registry
regionendpoint: https://s3.us-east-1.amazonaws.com # required for MinIO
rootdirectory: /v2 # prefix in bucket
encrypt: true # SSE-S3 encryption
multipartcopychunksize: 33554432 # 32MB chunk for multipart
multipartcopymaxconcurrency: 100
multipartcopythresholdsize: 33554432
Garbage collection
Deleting an image tag in Harbor performs a soft delete — the tag is removed from the metadata database, but the blobs remain in storage. To reclaim disk space, you must run garbage collection (GC).
# Docker Compose: run GC manually
docker compose exec registryctl /home/harbor/harbor_registryctl garbage-collect
# Or trigger via API
curl -X POST "https://registry.example.com/api/v2.0/system/gc/schedule" \
-H "Content-Type: application/json" \
-u "admin:password" \
-d '{"schedule":{"type":"Manual"}}'
# Schedule automatic GC (e.g., weekly at 2 AM Saturday)
curl -X PUT "https://registry.example.com/api/v2.0/system/gc/schedule" \
-H "Content-Type: application/json" \
-u "admin:password" \
-d '{"schedule":{"type":"Weekly","cron":"0 0 2 * * 6"}}'
Since Harbor v2.1, garbage collection is non-blocking — you can push, pull, and delete artifacts while GC runs in the background without placing the registry in read-only mode. For large registries (multi-TB), GC can still take hours. You can configure the number of parallel GC workers to control resource usage. Despite being non-blocking, it is still good practice to schedule GC during off-peak hours to minimize resource contention.
Cost optimization for large registries
- Tag retention policies — Automatically delete old tags. Keep the last N tags, or tags newer than N days. Combined with GC, this keeps storage manageable.
- S3 Intelligent-Tiering — Image layers that haven't been pulled in months move to cheaper storage automatically.
- Deduplication — Docker Distribution already deduplicates blobs (layers shared across images are stored once). This is automatic and significant — base images shared across hundreds of apps are stored once.
- Monitor storage growth — Track the
harbor_project_quota_usage_bytesmetric. Set alerts when total storage exceeds thresholds.
Replication
Harbor supports replicating images between Harbor instances and between Harbor and other registries (Docker Hub, GCR, ECR, ACR, Quay, GitLab, etc.). Replication is a key feature for multi-datacenter deployments, disaster recovery, and hybrid cloud architectures.
Replication modes
Push-Based
The source Harbor instance pushes images to the destination. Use this when the source is in a trusted zone and can reach the destination.
- Source initiates the transfer
- Good for CI/CD pipelines — push to staging, replicate to production
- Event-driven: trigger on push, or scheduled
Pull-Based
The destination Harbor instance pulls images from the source. Use this when the destination is in a restricted zone that cannot accept inbound connections.
- Destination initiates the transfer
- Good for air-gapped or edge sites that periodically sync
- Scheduled: cron-based sync intervals
Replication policies
Policies define what gets replicated, with fine-grained filters:
- By project — Replicate all images in specific projects
- By repository — Filter by repository name pattern (e.g.,
myproject/api-*) - By tag — Filter by tag pattern (e.g.,
v*for release tags only, excludelatest) - By label — Replicate only images with specific labels (e.g.,
approved-for-production) - By resource type — Images, Helm charts, or both
# Create a replication policy via API
curl -X POST "https://registry.example.com/api/v2.0/replication/policies" \
-H "Content-Type: application/json" \
-u "admin:password" \
-d '{
"name": "prod-sync",
"src_registry": {"id": 1},
"dest_namespace": "production",
"trigger": {
"type": "event_based"
},
"filters": [
{"type": "name", "value": "myproject/**"},
{"type": "tag", "value": "v*"}
],
"enabled": true,
"override": true
}'
Cross-datacenter DR pattern
A common production pattern for disaster recovery:
- Primary site (DC1) — Harbor with S3 storage, receives all pushes from CI/CD
- DR site (DC2) — Harbor with its own S3 storage, receives push-based replication from DC1
- Replication policy: event-driven, replicate all projects, all tags
- DNS failover: if DC1 is down, switch
registry.example.comto DC2 - RTO depends on DNS TTL and replication lag (typically minutes for event-driven replication)
Container images are large. A single image can be 500 MB - 2 GB. Replicating hundreds of images across datacenters saturates WAN links. Use tag filters to replicate only what's needed at the destination, not everything. Consider scheduling bulk replication during off-peak hours and using event-based replication only for critical images.
Security
Security is Harbor's strongest differentiator over a bare Docker Distribution registry. The combination of vulnerability scanning, image signing, RBAC, and policy enforcement makes Harbor a supply chain security tool, not just a registry.
Scanning Trivy Integration
- Scan images automatically on push (scan-on-push)
- Manual scan via UI or API
- Severity levels: Critical, High, Medium, Low, Negligible
- Block pulls of images exceeding a severity threshold
- CVE allowlisting for accepted vulnerabilities
Signing Content Trust
Access RBAC & Auth
- Project-level roles: Admin, Maintainer (formerly Master), Developer, Guest, Limited Guest
- Robot accounts for CI/CD (scoped to specific projects and actions)
- OIDC integration (Keycloak, Azure AD, Okta, Dex)
- LDAP/AD for enterprise user directories
- Audit logs for all operations
Policy Enforcement
- Immutable tags: prevent overwriting (e.g.,
v1.0.0cannot be re-pushed) - Vulnerability prevention policy: block pulling images with critical CVEs
- Tag retention policies: auto-delete old/untagged images
- Network policies: restrict which pods can reach Harbor (K8s)
Scan-on-push configuration
# Enable scan-on-push for a project via API
curl -X PUT "https://registry.example.com/api/v2.0/projects/myproject" \
-H "Content-Type: application/json" \
-u "admin:password" \
-d '{"metadata": {"auto_scan": "true"}}'
# Set a vulnerability prevention policy (block critical CVEs)
curl -X PUT "https://registry.example.com/api/v2.0/projects/myproject" \
-H "Content-Type: application/json" \
-u "admin:password" \
-d '{"metadata": {"prevent_vul": "true", "severity": "critical"}}'
Robot accounts for CI/CD
# Create a robot account scoped to a project
curl -X POST "https://registry.example.com/api/v2.0/robots" \
-H "Content-Type: application/json" \
-u "admin:password" \
-d '{
"name": "ci-pipeline",
"duration": -1,
"level": "project",
"permissions": [
{
"namespace": "myproject",
"kind": "project",
"access": [
{"resource": "repository", "action": "push"},
{"resource": "repository", "action": "pull"},
{"resource": "tag", "action": "create"}
]
}
]
}'
# Response includes the robot token -- store it securely!
Image signing with Cosign
# Sign an image with Cosign (keyless, via Sigstore)
cosign sign --yes registry.example.com/myproject/myapp:v1.2
# Sign with a private key
cosign sign --key cosign.key registry.example.com/myproject/myapp:v1.2
# Verify a signature
cosign verify --key cosign.pub registry.example.com/myproject/myapp:v1.2
# Verify keyless signature (checks Fulcio cert + Rekor transparency log)
cosign verify \
--certificate-identity=ci@example.com \
--certificate-oidc-issuer=https://accounts.google.com \
registry.example.com/myproject/myapp:v1.2
TLS configuration
Harbor must run with TLS in production. Docker and Podman refuse to push/pull from non-HTTPS registries unless explicitly configured as insecure (which you should never do in production).
# Generate a certificate (Let's Encrypt recommended for public-facing)
# For internal registries, use your organization's CA
openssl req -x509 -nodes -days 365 \
-newkey rsa:4096 \
-keyout /data/cert/server.key \
-out /data/cert/server.crt \
-subj "/CN=registry.example.com" \
-addext "subjectAltName=DNS:registry.example.com"
# Distribute the CA cert to all Docker/Podman clients
# Docker:
mkdir -p /etc/docker/certs.d/registry.example.com/
cp ca.crt /etc/docker/certs.d/registry.example.com/ca.crt
# Podman:
cp ca.crt /etc/containers/certs.d/registry.example.com/ca.crt
The full Harbor security stack for a production deployment: TLS everywhere + OIDC authentication + project-level RBAC + robot accounts for CI/CD + scan-on-push with Trivy + vulnerability prevention policy (block critical CVEs from being pulled) + Cosign image signing (keyless via Sigstore in CI/CD) + immutable tags for release versions + Kyverno or OPA Gatekeeper for admission control. This gives you end-to-end provenance and enforcement.
Image Management
Harbor organizes images into projects, which are the fundamental unit of access control and policy enforcement. Understanding the hierarchy and management features is essential for keeping a registry healthy at scale.
Project hierarchy
- Project — Top-level container (e.g.,
myproject). Maps to the first path segment in the image name:registry.example.com/myproject/myapp:v1 - Repository — An image name within a project (e.g.,
myproject/myapp). Contains multiple tags. - Tag — A specific version of an image (e.g.,
v1.2.3,latest,sha-abc123) - Label — User-defined metadata attached to images for filtering, replication, and organization
Tag retention policies
Without retention policies, registries grow unbounded. Every CI/CD pipeline push adds a new tag. Retention policies automatically clean up old tags.
# Create a tag retention policy via API
# Keep the 10 most recent tags matching "v*" and always keep "latest"
curl -X POST "https://registry.example.com/api/v2.0/retentions" \
-H "Content-Type: application/json" \
-u "admin:password" \
-d '{
"algorithm": "or",
"rules": [
{
"action": "retain",
"template": "latestPushedK",
"params": {"latestPushedK": 10},
"tag_selectors": [{"kind": "doublestar", "decoration": "matches", "pattern": "v*"}],
"scope_selectors": {"repository": [{"kind": "doublestar", "decoration": "repoMatches", "pattern": "**"}]}
},
{
"action": "retain",
"template": "always",
"tag_selectors": [{"kind": "doublestar", "decoration": "matches", "pattern": "latest"}],
"scope_selectors": {"repository": [{"kind": "doublestar", "decoration": "repoMatches", "pattern": "**"}]}
}
],
"trigger": {"kind": "Schedule", "settings": {"cron": "0 0 3 * * *"}}
}'
Proxy cache (pull-through cache)
Harbor can act as a pull-through cache for remote registries. When a client pulls registry.example.com/dockerhub-cache/library/nginx:latest, Harbor checks its local cache first. If the image is not cached or is stale, Harbor pulls it from Docker Hub, caches it locally, and serves it to the client.
- Reduces Docker Hub rate limiting — Unauthenticated pulls are limited to 100 per 6 hours, Docker Personal accounts to 200 per 6 hours (paid subscribers get unlimited pulls). A proxy cache means only the first pull hits Docker Hub.
- Bandwidth savings — Images are pulled once from the internet, then served locally at LAN speed.
- Resilience — If Docker Hub is down, cached images are still available.
- Supports: Docker Hub, GCR, Quay, GitHub Container Registry, ECR, and any OCI-compliant registry.
# Create a proxy cache project for Docker Hub
# In the Harbor UI: Projects > New Project > Proxy Cache
# Or via API:
curl -X POST "https://registry.example.com/api/v2.0/projects" \
-H "Content-Type: application/json" \
-u "admin:password" \
-d '{
"project_name": "dockerhub-cache",
"registry_id": 1,
"metadata": {"public": "true"}
}'
# Pull through the cache (from any Docker client):
docker pull registry.example.com/dockerhub-cache/library/nginx:1.25
Quota management
Projects can have storage quotas to prevent any single team or project from consuming all available storage:
# Set a 50 GB quota on a project
curl -X PUT "https://registry.example.com/api/v2.0/projects/myproject" \
-H "Content-Type: application/json" \
-u "admin:password" \
-d '{"storage_limit": 53687091200}' # 50 GB in bytes
Multi-architecture images
Harbor fully supports OCI image indexes (multi-arch manifests). Push multi-platform images with docker buildx or crane, and Harbor stores and serves the correct architecture automatically:
# Build and push a multi-arch image
docker buildx create --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag registry.example.com/myproject/myapp:v1.2 \
--push .
Enable immutable tags on production projects. Once v1.2.3 is pushed, it cannot be overwritten. This prevents accidental or malicious overwrites of release artifacts. CI/CD pipelines should use unique tags (git SHA, build number) and only tag specific versions as immutable releases. The latest tag should not be immutable — it needs to float.
Upgrades
Harbor upgrades involve database migrations and container image updates. The upgrade process differs between Docker Compose and Helm deployments, but both require careful planning.
Pre-upgrade checklist
- Read the release notes — Check for breaking changes, deprecated features, and migration notes at
github.com/goharbor/harbor/releases - Back up the database —
pg_dumpthe entire Harbor database. This is non-negotiable. - Back up the configuration —
harbor.yml, TLS certificates, custom configs - Test in staging first — Restore the database backup to a staging instance and upgrade there
- Check the upgrade path — Harbor supports upgrading from N-2 to N (two previous minor versions). Skipping more than two minor versions (e.g., 2.10 to 2.14) requires stepping through intermediate versions.
Docker Compose upgrade
# 1. Back up the database
docker compose exec harbor-db pg_dump -U postgres registry > harbor-backup-$(date +%Y%m%d).sql
# 2. Back up the config
cp harbor.yml harbor.yml.bak
cp -r /data/secret /data/secret.bak
# 3. Stop Harbor
docker compose down
# 4. Download the new version
wget https://github.com/goharbor/harbor/releases/download/v2.14.3/harbor-offline-installer-v2.14.3.tgz
tar xzf harbor-offline-installer-v2.14.3.tgz -C /opt/harbor-new
# 5. Copy your existing harbor.yml to the new directory
cp harbor.yml /opt/harbor-new/harbor/harbor.yml
# 6. Run the migration tool (if required by release notes)
docker run -it --rm \
-v /data/database:/var/lib/postgresql/data \
goharbor/harbor-db-migrator:v2.14.3 up
# 7. Run the installer
cd /opt/harbor-new/harbor
./install.sh --with-trivy
# 8. Verify
docker compose ps
curl -s https://registry.example.com/api/v2.0/systeminfo | jq .harbor_version
Helm chart upgrade
# 1. Back up the external database
pg_dump -h harbor-pg.rds.amazonaws.com -U harbor -d registry > harbor-backup.sql
# 2. Update the Helm repo
helm repo update harbor
# 3. Check what will change
helm diff upgrade harbor harbor/harbor \
--namespace harbor \
--values harbor-values.yaml
# 4. Perform the upgrade
helm upgrade harbor harbor/harbor \
--namespace harbor \
--values harbor-values.yaml \
--timeout 10m
# 5. Watch the rollout
kubectl -n harbor rollout status deployment/harbor-core
kubectl -n harbor get pods
Harbor database migrations are not reversible. If an upgrade fails after the migration runs, you cannot simply roll back to the previous Harbor version — the database schema has changed. Always have a database backup you have tested restoring. "We backed it up" is not the same as "we verified we can restore it."
Upgrade path rules
| Scenario | Supported? | Notes |
|---|---|---|
| 2.13.x to 2.14.x | Yes | N-1 minor version upgrade, straightforward |
| 2.12.x to 2.14.x | Yes | N-2 minor version upgrade, supported directly |
| 2.14.0 to 2.14.3 | Yes | Patch upgrades are safe, no DB migration usually |
| 2.10.x to 2.14.x | No (step required) | Exceeds N-2 range. Step through intermediate versions (e.g., 2.10 → 2.12 → 2.14) |
| 1.x to 2.x | Complex | Major version jump with significant schema changes. Follow the official migration guide carefully |
Helm upgrades on Kubernetes perform a rolling update of deployments. During the rollout, old and new pods coexist. Ensure all replicas are running the same Harbor version before declaring the upgrade complete. If the new pods fail health checks, Kubernetes will automatically roll back the deployment — but the database migration may have already run. This is why database backup is critical even on K8s.
Monitoring
Harbor exposes Prometheus-compatible metrics and health check endpoints. Production deployments need proactive monitoring to catch storage issues, scan backlogs, and authentication failures before they become incidents.
Key metrics
| Metric | Alert Threshold | Why |
|---|---|---|
| harbor_project_quota_usage_bytes | > 80% of quota | Project approaching quota limit, pushes will fail |
| harbor_task_queue_size | > 100 pending | Scan or replication backlog growing — job service may be overwhelmed |
| harbor_artifact_pulled | Baseline deviation | Unusual pull patterns may indicate security events or outages |
| harbor_artifact_pushed | Baseline deviation | Sudden spike may indicate CI/CD flood or misconfigured pipeline |
| Storage backend usage | > 80% capacity | Running out of storage is a registry-down event |
| Scan queue depth | > 50 pending | Trivy scanner falling behind. May need more replicas or resources. |
| Database connections | > 80% of max | Connection exhaustion causes Harbor to become unresponsive |
| Replication lag | > 1 hour | DR site is stale. Network or destination may be down. |
Prometheus metrics endpoint
# Enable metrics in harbor.yml
metric:
enabled: true
type: prometheus
port: 9090
path: /metrics
# Prometheus scrape config
- job_name: 'harbor'
scrape_interval: 30s
metrics_path: /metrics
static_configs:
- targets:
- harbor-core:9090
- harbor-registry:9090
- harbor-jobservice:9090
- harbor-exporter:9090
Health check endpoints
# Overall health check
curl -s https://registry.example.com/api/v2.0/health | jq
# Returns component status for each service:
# {
# "status": "healthy",
# "components": [
# {"name": "core", "status": "healthy"},
# {"name": "database", "status": "healthy"},
# {"name": "redis", "status": "healthy"},
# {"name": "registry", "status": "healthy"},
# {"name": "registryctl", "status": "healthy"},
# {"name": "trivy", "status": "healthy"}
# ]
# }
# Use in Kubernetes liveness/readiness probes
# The Helm chart configures these automatically
Grafana dashboard
Community Grafana dashboards are available for Harbor. Key panels to include:
- Push/pull rate over time (operations per minute)
- Storage usage per project (trend line with forecast)
- Scan results distribution (pie chart: critical, high, medium, low)
- Replication status (success/failure counts, lag time)
- API response latency (p50, p95, p99)
- Active robot accounts and their last-used timestamps
Log aggregation
# Docker Compose: logs go to /var/log/harbor/ by default
# Configure log rotation in harbor.yml:
log:
level: info # debug, info, warning, error, fatal
local:
rotate_count: 50
rotate_size: 200M
location: /var/log/harbor
# For K8s: use a log aggregator (Loki, ELK, Fluentd)
# Harbor pods write to stdout/stderr (standard K8s logging)
At minimum, monitor three things: storage usage (the number one cause of Harbor outages), health endpoint (any component going unhealthy), and database connection count (connection exhaustion makes Harbor unresponsive). Everything else is important but these three catch the most common production issues.
Licensing
Harbor is licensed under Apache 2.0 and is a CNCF Graduated project. There is no enterprise edition, no feature gating, no paid tier. Every feature is available in the open-source release. This is a significant differentiator in the registry space, where most competitors have commercial editions with gated features.
CNCF Graduated status
CNCF Graduation means Harbor has met the foundation's highest maturity bar:
- Adopted by a diverse set of production users
- Passed an independent security audit
- Has a healthy contributor ecosystem (not single-vendor dominated)
- Follows CNCF governance practices
Commercial support
While Harbor itself is free, commercial support is available:
- VMware/Broadcom — Harbor was originally created by VMware. VMware Tanzu products include Harbor as a supported component. Following the Broadcom acquisition, the support landscape is evolving.
- Community support — GitHub issues, CNCF Slack (#harbor channel), community mailing list. Response times vary.
- Third-party consultants — Several CNCF-ecosystem consultancies offer Harbor deployment and support.
Registry comparison (licensing & cost)
| Registry | License | Self-Hosted | Cost Model | Enterprise Features |
|---|---|---|---|---|
| Harbor | Apache 2.0 | Yes (only) | Free (infra only) | All included |
| Quay | Apache 2.0 | Yes + managed | Free OSS / RHEL sub for support | All in OSS, support requires sub |
| JFrog Artifactory | Proprietary | Yes + SaaS | $150+/month (Pro tier) | Many features gated to paid tiers |
| Sonatype Nexus | EPL (Community) / Proprietary (Pro) | Yes | Free Community / paid Pro | Docker support available in Community Edition (formerly OSS) |
| Docker Hub | Proprietary | No | Free + paid tiers | Paid features (Scout, teams) |
| ECR / GCR / ACR | Proprietary | No | Storage + egress | Cloud-native integration |
For most organizations, Harbor's zero-cost licensing is a major advantage. The total cost of ownership is just infrastructure (compute, storage, database). A production Harbor on Kubernetes with RDS and S3 typically costs $200-500/month depending on scale — compared to thousands per month for commercial registries at equivalent scale. The trade-off is operational responsibility: you run it, you maintain it.
Consultant's Checklist
Before deploying Harbor for a client, work through these questions:
- Deployment platform? — Kubernetes (Helm) or Docker Compose on VMs? K8s is strongly preferred for production.
- Storage backend? — S3/MinIO for HA and scalability. Filesystem only for single-node dev setups. Estimate initial storage: how many images, how large, how fast will it grow?
- Database? — External PostgreSQL with HA (RDS, Patroni). Never use the built-in database for production.
- HA requirements? — Multiple Harbor replicas + external DB/Redis + S3 storage + load balancer. Or is single-node acceptable?
- Authentication? — OIDC (Keycloak, Azure AD, Okta) or LDAP? How will CI/CD authenticate (robot accounts)?
- Security scanning? — Scan-on-push enabled? Vulnerability prevention policy (block critical CVEs)? Air-gapped Trivy DB?
- Image signing? — Cosign (recommended; Notary v1 was removed in Harbor v2.9). Keyless (Sigstore) or key-based? Verification policies in the deployment pipeline (Kyverno, OPA Gatekeeper)?
- Replication? — Multi-site deployment? DR requirements? Push or pull mode? Bandwidth between sites?
- Proxy cache? — Cache Docker Hub pulls to avoid rate limiting? Cache other upstream registries?
- Retention and GC? — Tag retention policies per project. GC schedule. Maintenance window for GC.
- TLS certificates? — Let's Encrypt, internal CA, or manual? Certificate distribution to all Docker/K8s clients.
- Monitoring? — Prometheus metrics, health checks, storage alerts, scan backlog alerts. Grafana dashboard.
- Backup strategy? — Database backups (pg_dump + WAL archiving). Config backups. S3 bucket versioning for registry blobs.
- Upgrade plan? — Who owns upgrades? What's the upgrade cadence? Staging environment for testing upgrades?
Small (< 100 images, < 10 users): Single node, Docker Compose, filesystem storage, built-in DB. 4 vCPU, 8 GB RAM, 100 GB disk.
Medium (100-1000 images, 10-100 users): 2-3 replicas on K8s, external PostgreSQL, S3 storage. 8 vCPU total, 16 GB RAM total, S3 unlimited.
Large (> 1000 images, > 100 users): 3+ replicas, RDS Multi-AZ, ElastiCache, S3 with lifecycle policies. 16+ vCPU total, 32+ GB RAM total.