My Setup #
In this tutorial I’m using the following kubeadm based Kubernetes cluster, with Nginx Ingress Controller and NFS CSI:
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ubuntu1 Ready control-plane 167d v1.29.11 192.168.30.10 <none> Ubuntu 24.04.1 LTS 6.8.0-49-generic containerd://1.7.23
ubuntu2 Ready worker 167d v1.29.11 192.168.30.11 <none> Ubuntu 24.04.1 LTS 6.8.0-49-generic containerd://1.7.23
ubuntu3 Ready worker 167d v1.29.11 192.168.30.12 <none> Ubuntu 24.04.1 LTS 6.8.0-49-generic containerd://1.7.23
ubuntu4 Ready worker 167d v1.29.11 192.168.30.13 <none> Ubuntu 24.04.1 LTS 6.8.0-49-generic containerd://1.7.23
My NFS CSI storage class looks like this:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io # NFS CSI Driver
parameters:
server: 192.168.30.15
share: /srv/nfs/k8s_nfs-csi
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
- nfsvers=3
Harbor Setup #
Create Namespace #
# Create "harbor" namespace
kubectl create namespace harbor
Kubernetes Secret #
I’m using an Let’s Encrypt wildcard certificate in this tutorial, since my K8s cluster is running locally.
# Create a Kubernetes secret for the TLS certificate
kubectl create secret tls harbor-tls \
--namespace harbor \
--cert=./fullchain.pem \
--key=./privkey.pem
Helm #
Install Helm #
Make sure Helm is installed:
# Install Helm with script
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 &&
chmod +x get_helm.sh &&
./get_helm.sh
# Verify the installation / check version
helm version
Add Helm Repository #
# Add the Harbor Helm repository
helm repo add harbor https://helm.goharbor.io
# Update all Helm repositories
helm repo update
# Optional: List available charts
helm search repo harbor -l
Adapt Helm Values #
# Optional: Save the Helm Chart values
helm show values harbor/harbor > harbor-values.yaml
# Adapt the values / create a configuration for the values
vi harbor-values.yaml
expose:
type: ingress
tls:
enabled: true
certSource: secret
secret:
secretName: "harbor-tls" # Define secret name
ingress:
hosts:
core: harbor.jklug.work
kubeVersionOverride: ""
className: nginx
annotations:
ingress.kubernetes.io/ssl-redirect: "true"
ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "0"
# ingress-specific labels
labels: {}
externalURL: https://harbor.jklug.work
persistence:
enabled: true
resourcePolicy: "keep"
persistentVolumeClaim:
registry:
existingClaim: ""
storageClass: "nfs-csi" # Define NFS CSI Storage Class
subPath: ""
accessMode: ReadWriteMany # Necessary to scale the registry
size: 5Gi
annotations: {}
jobservice:
jobLog:
existingClaim: ""
storageClass: "nfs-csi" # Define NFS CSI Storage Class
subPath: ""
accessMode: ReadWriteOnce
size: 1Gi
annotations: {}
database:
existingClaim: ""
storageClass: "nfs-csi" # Define NFS CSI Storage Class
subPath: ""
accessMode: ReadWriteOnce
size: 1Gi
annotations: {}
redis:
existingClaim: ""
storageClass: "nfs-csi" # Define NFS CSI Storage Class
subPath: ""
accessMode: ReadWriteOnce
size: 1Gi
annotations: {}
trivy:
existingClaim: ""
storageClass: "nfs-csi" # Define NFS CSI Storage Class
subPath: ""
accessMode: ReadWriteOnce
size: 5Gi
annotations: {}
# Optional: Scale the registry
registry:
replicas: 3
Install Harbor #
# Install Harbor
helm install harbor harbor/harbor \
-n harbor \
-f harbor-values.yaml
# Shell output:
NAME: harbor
LAST DEPLOYED: Fri May 9 17:50:50 2025
NAMESPACE: harbor
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Please wait for several minutes for Harbor deployment to complete.
Then you should be able to visit the Harbor portal at https://harbor.jklug.work
For more details, please visit https://github.com/goharbor/harbor
# Update Harbor
helm upgrade harbor harbor/harbor \
-n harbor \
-f harbor-values.yaml
# Uninstall Harbor
helm delete harbor -n harbor
Verify Installation #
Verify Pods #
# Verify pods: Scaled registry
kubectl get pod -o wide -n harbor
# Shell output:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
harbor-core-7d86d8ccb6-9zwnw 1/1 Running 0 95s 10.0.1.102 ubuntu2 <none> <none>
harbor-database-0 1/1 Running 0 95s 10.0.3.28 ubuntu4 <none> <none>
harbor-jobservice-db5774fbd-8wq8w 1/1 Running 2 (44s ago) 95s 10.0.3.29 ubuntu4 <none> <none>
harbor-portal-5769bdf64d-z4tlc 1/1 Running 0 95s 10.0.2.169 ubuntu3 <none> <none>
harbor-redis-0 1/1 Running 0 95s 10.0.2.147 ubuntu3 <none> <none>
harbor-registry-847bb97c96-5wh8d 2/2 Running 0 95s 10.0.1.111 ubuntu2 <none> <none>
harbor-registry-847bb97c96-mxvhf 2/2 Running 0 95s 10.0.2.209 ubuntu3 <none> <none>
harbor-registry-847bb97c96-zs9gq 2/2 Running 0 95s 10.0.3.197 ubuntu4 <none> <none>
harbor-trivy-0 1/1 Running 0 95s 10.0.1.192 ubuntu2 <none> <none>
Verify Ingress Resource #
# Verify the Ingress resource
kubectl get ingress -n harbor
# Shell output:
NAME CLASS HOSTS ADDRESS PORTS AGE
harbor-ingress nginx harbor.jklug.work 192.168.30.200 80, 443 118s
# List Ingress details:
kubectl describe ingress -n harbor
# Shell output: (Verify the correct secret)
...
Namespace: harbor
Address: 192.168.30.200
Ingress Class: nginx
Default backend: <default>
TLS:
harbor-tls terminates harbor.jklug.work # Verify the correct secret is used
Verify PVC & PV #
# List PVC
kubectl get pvc -n harbor
# Shell output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
data-harbor-redis-0 Bound pvc-b8f32c44-2bc3-481e-94c2-a8bc1297f9c1 1Gi RWO nfs-csi <unset> 3m6s
data-harbor-trivy-0 Bound pvc-517aa171-a1da-4e2d-b58a-252e5f86c314 5Gi RWO nfs-csi <unset> 3m6s
database-data-harbor-database-0 Bound pvc-6a44f273-b1a1-402e-a5fa-d2da172c6252 1Gi RWO nfs-csi <unset> 3m6s
harbor-jobservice Bound pvc-804753c2-8db7-4d54-8b08-04c754d43119 1Gi RWO nfs-csi <unset> 3m6s
harbor-registry Bound pvc-9784aff5-74a7-4f30-a0bc-c16909939b05 5Gi RWX nfs-csi <unset> 3m6s
# List PV
kubectl get pv
# Shell output:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
pvc-517aa171-a1da-4e2d-b58a-252e5f86c314 5Gi RWO Delete Bound harbor/data-harbor-trivy-0 nfs-csi <unset> 3m31s
pvc-6a44f273-b1a1-402e-a5fa-d2da172c6252 1Gi RWO Delete Bound harbor/database-data-harbor-database-0 nfs-csi <unset> 3m30s
pvc-804753c2-8db7-4d54-8b08-04c754d43119 1Gi RWO Delete Bound harbor/harbor-jobservice nfs-csi <unset> 3m31s
pvc-9784aff5-74a7-4f30-a0bc-c16909939b05 5Gi RWX Delete Bound harbor/harbor-registry nfs-csi <unset> 3m30s
pvc-b8f32c44-2bc3-481e-94c2-a8bc1297f9c1 1Gi RWO Delete Bound harbor/data-harbor-redis-0 nfs-csi <unset> 3m31s
DNS Entry #
# Add a DNS entry
192.168.30.200 harbor.jklug.work
Access Harbor Webinterface #
Harbor GUI: https://harbor.jklug.work
# Default username:
admin
# Default password:
Harbor12345
API Commands #
Here are some API commands, that I have tested with Harbor.
Create Project / Repository #
curl -X POST "https://harbor.jklug.work/api/v2.0/projects" \
-u "admin:Harbor12345" \
-H "Content-Type: application/json" \
-d '{
"project_name": "example-project",
"metadata": {
"public": "false"
}
}'
Create User #
curl -X POST "https://harbor.jklug.work/api/v2.0/users" \
-u "admin:Harbor12345" \
-H "Content-Type: application/json" \
-d '{
"username": "example-user",
"email": "example-user@example.com",
"password": "myStrongPW123!",
"realname": "Example User",
"comment": "Some new user"
}'
Add User to Project #
Add user with the role “Developer” to project:
curl -X POST "https://harbor.jklug.work/api/v2.0/projects/example-project/members" \
-u "admin:Harbor12345" \
-H "Content-Type: application/json" \
-d '{
"role_id": 2,
"member_user": {
"username": "example-user"
}
}'
Push & Pull Container #
Docker Login #
# Login to Harbor container registry
docker login harbor.jklug.work
# Shell output:
Username: admin
Password:
WARNING! Your credentials are stored unencrypted in '/home/ubuntu/.docker/config.json'.
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/
Login Succeeded
Tag Local Container Image #
# Pull a image, for example Nginx
docker pull nginx:latest
# Tag the local image
docker tag nginx:latest harbor.jklug.work/example-project/nginx:latest
# Verify the image tag
docker images
# Shell output:
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest a830707172e8 3 weeks ago 192MB
harbor.jklug.work/example-project/nginx latest a830707172e8 3 weeks ago 192MB
Push Image #
# Push the image to the Harbor registry
docker push harbor.jklug.work/example-project/nginx:latest
# Shell output:
8030dd26ec5d: Pushed
d84233433437: Pushed
f8455d4eb3ff: Pushed
286733b13b0f: Pushed
46a24b5c31d8: Pushed
84accda66bf0: Pushed
6c4c763d22d0: Pushed
latest: digest: sha256:056c8ad1921514a2fc810a792b5bd18a02d003a99d6b716508bf11bc98c413c3 size: 1778
Pull Image #
# Pull the image from the Harbor registry
docker pull harbor.jklug.work/example-project/nginx:latest
Vulnerability Scan #





Links #
# Harbor Official Documentation
https://goharbor.io/docs/2.13.0/install-config/harbor-ha-helm/