Skip to main content

Habor Container Registry - Kubernetes Deployment with Helm

1110 words·
Harbor Container Registry Kubernetes Helm Container Vulnerability Scan
Table of Contents

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/