Skip to main content

Harbor — Self-Hosted Container Registry

Harbor is an enterprise-grade private container registry. It stores your Docker images inside your cluster, scans them for vulnerabilities, enforces access control, and integrates with your CI/CD pipelines — no Docker Hub dependency, no image leak risk.


Why a Private Registry

Without Harbor:
CI builds image → pushes to Docker Hub (public or private)
k3s pulls from Docker Hub → needs internet, rate-limited, slow

With Harbor:
CI builds image → pushes to harbor.yourdomain.com (internal)
k3s pulls from harbor.yourdomain.com → fast, no internet needed
Images never leave your network

Key Features

FeatureWhat It Does
Multi-project registrySeparate registries per team/project
RBACPush/pull permissions per user or robot account
Vulnerability scanningTrivy integration — scans every pushed image
Image signingCosign/Notary support
ReplicationMirror images to/from other registries
Garbage collectionAutomatically clean untagged images
Web UIBrowse images, tags, scan reports

Install Harbor via Helm

helm repo add harbor https://helm.goharbor.io

helm install harbor harbor/harbor \
--namespace harbor \
--create-namespace \
--set expose.type=loadBalancer \
--set expose.loadBalancer.IP=10.0.0.210 \
--set expose.tls.enabled=false \
--set externalURL=http://10.0.0.210 \
--set persistence.persistentVolumeClaim.registry.storageClass=longhorn \
--set persistence.persistentVolumeClaim.registry.size=100Gi \
--set persistence.persistentVolumeClaim.database.storageClass=longhorn \
--set persistence.persistentVolumeClaim.redis.storageClass=longhorn \
--set harborAdminPassword=Admin12345

Wait for all pods:

kubectl get pods -n harbor --watch

Access Harbor UI

Open: http://10.0.0.210

Login: admin / Admin12345

Via Tailscale: http://10.0.0.210 (after subnet route) Via Cloudflare: https://registry.yourdomain.com


Configure k3s to Use Harbor

Tell k3s to use Harbor as a registry mirror:

sudo nano /etc/rancher/k3s/registries.yaml
mirrors:
"harbor.local":
endpoint:
- "http://10.0.0.210"

configs:
"10.0.0.210":
auth:
username: admin
password: Admin12345
sudo systemctl restart k3s

Push an Image to Harbor

# Create project in Harbor UI first: "platform"

# Tag and push
docker tag my-app:latest 10.0.0.210/platform/my-app:latest
docker push 10.0.0.210/platform/my-app:latest

Configure Docker Daemon (local machine)

Add Harbor as an insecure registry (if using HTTP):

// /etc/docker/daemon.json
{
"insecure-registries": ["10.0.0.210"]
}
sudo systemctl restart docker
docker login 10.0.0.210

Robot Accounts for CI/CD

Create robot accounts in Harbor UI for CI pipelines:

Harbor UI → Projects → platform → Robot Accounts
→ New Robot Account
Name: gitlab-ci
Permissions: push + pull
→ Copy generated token

In GitLab CI:

variables:
HARBOR_URL: "10.0.0.210"
HARBOR_PROJECT: "platform"

build:
script:
- docker login $HARBOR_URL -u robot\$gitlab-ci -p $HARBOR_TOKEN
- docker build -t $HARBOR_URL/$HARBOR_PROJECT/my-app:$CI_COMMIT_SHA .
- docker push $HARBOR_URL/$HARBOR_PROJECT/my-app:$CI_COMMIT_SHA

Vulnerability Scanning

Harbor integrates Trivy — every pushed image is automatically scanned:

Harbor UI → Projects → platform → Repositories → my-app
→ Artifacts → sha256:abc...
→ Vulnerabilities tab:
CRITICAL: 0
HIGH: 2
MEDIUM: 5
LOW: 12

Set a policy to block deployments if CRITICAL vulnerabilities are found:

Harbor UI → Projects → platform → Configuration
→ Automatically scan images on push: ✔
→ Prevent vulnerable images from running: CRITICAL

ArgoCD will refuse to sync any image that fails the scan policy.


Done When

✔ Harbor pods Running with Longhorn storage
✔ UI accessible at 10.0.0.210
✔ k3s configured to pull from Harbor
✔ First image pushed and scannable
✔ Robot account created for CI/CD
✔ Vulnerability scanning active