# Deploying Harbor on Kubernetes: A Step-by-Step Guide

Harbor is a cloud-native registry that allows storing, signing, and scanning container images for vulnerabilities. Deploying Harbor on Kubernetes provides a scalable, highly available registry with integrated security features. In this guide, I will walk you through a full setup on a Kubernetes cluster using Helm, from installation to pushing your first Docker image.

---

## Preparing for Installation

Before starting, ensure that your Kubernetes cluster is ready and Helm is installed. You should also have an ingress controller configured if you plan to expose Harbor externally.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1763665014974/e52930ba-87be-4f7e-92af-8bde5b2c22ef.png align="center")

---

## Adding the Harbor Helm Repository

Helm simplifies deploying Harbor on Kubernetes. Begin by adding the Harbor Helm repository and fetching the chart:

```yaml
helm repo add harbor https://helm.goharbor.io
helm repo update
helm fetch harbor/harbor --untar
```

Harbor provides detailed documentation for a high-availability deployment:

[Harbor HA Helm Installation Guide](https://goharbor.io/docs/2.14.0/install-config/harbor-ha-helm/).

---

## Configuring Values for Your Deployment

Harbor is highly configurable through a values file. I created a `values.yaml` to suit my environment. Key configurations included:

* Exposing Harbor via ingress with TLS enabled using the `pangolin` certificate.
    
* Enabling persistence for Registry, Jobservice, and Trivy to ensure images are not lost during pod restarts.
    
* Configuring internal components like Redis and PostgreSQL.
    

**Values file excerpts:**

```yaml
expose:
  type: ingress
  tls:
    enabled: true
    certSource: pangolin
  ingress:
    hosts:
      core: harbor.nyzex.in
    className: "nginx"

externalURL: https://harbor.nyzex.in

persistence:
  enabled: true
  resourcePolicy: keep
  persistentVolumeClaim:
    registry:
      size: 10Gi
    jobservice:
      jobLog:
        size: 1Gi
    trivy:
      size: 5Gi

redis:
  enabled: true
  password: "redisStrongPassword"

harborAdminPassword: "Harbor12345"
```

We have disabled tls here, because it is being handled by pangolin for us. To understand that setup, please check my other blog:

[Exposing Kubernetes Services Over the Internet Using MetalLB, NGINX Ingress, and Pangolin](https://blog.nyzex.in/exposing-kubernetes-services-over-the-internet-using-metallb-nginx-ingress-and-pangolin)

---

## Installing Harbor via Helm

Once your values file is ready, install Harbor in the `harbor` namespace:

```yaml
kubectl create namespace harbor
helm install my-release ./harbor -n harbor -f values.yaml
```

Check the status of the pods and services:

```yaml
kubectl get pods -n harbor
kubectl get svc -n harbor
kubectl get ingress -n harbor
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1763665573997/ce0db74d-d4c3-4a48-9926-9cd98b9eb8a0.png align="center")

---

## Retrieving the Admin Password

Harbor generates an admin password automatically if not set. To retrieve it:

```yaml
kubectl get secret my-release-harbor-core -n harbor -o jsonpath="{.data.HARBOR_ADMIN_PASSWORD}" | base64 --decode
```

This password allows you to log in to the Harbor UI at `https://harbor.nyzex.in`.

---

## Fixing the Unauthorized Issue

After logging in, I encountered an unauthorized error while pushing images. The crucial fix was setting:

```yaml
registry:
  relativeurls: true
```

This change ensures Harbor correctly handles relative paths for the registry.

---

## Docker Workflow

After logging in, I tested Harbor by building and pushing a sample Docker image.

**Sample Dockerfile:**

```yaml
FROM busybox:latest
```

**Build and tag the image:**

```yaml
docker build -t harbor.nyzex.in/myproj/test-image:latest .
```

**Login to Harbor:**

```yaml
docker login harbor.nyzex.in
```

**Push the image:**

```yaml
docker push harbor.nyzex.in/myproj/test-image:latest
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1763665337086/709bae7d-48b4-4a05-bd20-06a60a40287d.png align="center")

The image successfully uploaded, and Harbor started scanning it if Trivy is enabled.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1763665725766/4d468cb1-f127-4482-a4ca-bc9064ca45c8.png align="center")

---

## Understanding Harbor Components and Persistence

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1763666358092/77ea5e16-359f-4038-bb17-c5886f3c39db.png align="center")

Harbor consists of several core components, each with its role:

* **Registry:** Stores Docker images. Requires persistent storage to avoid losing images.
    
* **Core:** Provides the UI, API, and handles authentication.
    
* **Jobservice:** Executes background jobs, such as image replication or garbage collection.
    
* **Redis:** Caching and session storage.
    
* **PostgreSQL:** Stores metadata, configurations, and user information.
    
* **Trivy (optional):** Performs vulnerability scans on images.
    

Persistent volumes ensure that each component retains data even if pods restart or are rescheduled.

```yaml
kubectl get pvc -n harbor
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1763665373771/a46b088e-1a12-4613-b148-4e1715ba7ee8.png align="center")

### **Harbor Registry (**`my-release-harbor-registry`)

* **Role:** This is the **core Docker registry**. All images you push or pull are stored here.
    
* **Storage:** `/storage` inside the pod (mapped to PVC `my-release-harbor-registry`).
    
* **Growth:** Every `docker push` increases this PVC.
    
* **Key takeaway:** This is the main storage to monitor and expand if needed.
    

### **Redis (**`my-release-harbor-redis`)

* **Role:** Redis is used as **cache and message broker** for Harbor. It speeds up operations like:
    
    * Session management
        
    * Job queue for replication
        
    * Temporary caching of metadata
        
* **Storage:** Minimal, mostly in-memory. PVC (`data-my-release-harbor-redis-0`) is 1Gi because Redis mostly uses RAM.
    
* **Growth:** Usually does **not grow with image pushes**; only used for transient data.
    

### **Trivy (**`my-release-harbor-trivy`)

* **Role:** Trivy is Harbor’s **vulnerability scanner**. It scans container images for CVEs.
    
* **Storage:** Stores vulnerability DB and scan cache in its PVC (`data-my-release-harbor-trivy-0`).
    
* **Growth:** Increases if you scan many images, because it caches scan results and the CVE database (~5Gi here).
    
* **Key takeaway:** You don’t need to increase this PVC unless you do tons of scans.
    

### **Database (**`my-release-harbor-database`)

* **Role:** Harbor’s **PostgreSQL database**. Stores:
    
    * Users, projects, and roles
        
    * Repository metadata
        
    * Scan results
        
    * Jobs, quotas, and configurations
        
* **Storage:** Your PVC is small (1Gi). Actual usage is tiny at first.
    
* **Growth:** Will grow slowly with metadata; pushing images does **not increase it significantly**.
    

### **Jobservice (**`my-release-harbor-jobservice`)

* **Role:** Manages **asynchronous jobs** for Harbor:
    
    * Image replication
        
    * Garbage collection
        
    * Scan jobs (Trivy)
        
    * Retention policies
        
* **Storage:** Uses its PVC (`my-release-harbor-jobservice`) minimally for job queues and logs (~1Gi).
    
* **Growth:** Typically small; no impact on image storage.
    

**Summary:**

| Component | PVC used | Grows with image push? | Notes |
| --- | --- | --- | --- |
| Registry | `my-release-harbor-registry` | Yes | This is your main image storage |
| Redis | `data-my-release-harbor-redis-0` | No | Only cache, ephemeral data |
| Trivy | `data-my-release-harbor-trivy-0` | Slightly | Stores scan DB & results |
| Database | `database-data-my-release-harbor-database-0` | Minor | Stores metadata |
| Jobservice | `my-release-harbor-jobservice` | Minor | Handles background jobs |

### Quick check of `/storage`

Now that we know `/storage` is to be monitored for usage, we can use the following to keep check on it:

```yaml
kubectl exec -n harbor -it deployment/my-release-harbor-registry -- sh -c "du -sh /storage"
```

This gives total usage.

### Check usage per subdirectory (`blobs` and `repositories`)

```yaml
kubectl exec -n harbor -it deployment/my-release-harbor-registry -- sh -c "du -sh /storage/*"
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1763665485335/0463af93-e97a-4879-885a-d65fce95622f.png align="center")

---

## Lessons Learned

* The `registry.relativeurls` fix is crucial to avoid push failures.
    
* Persistence ensures images, logs, and scan data are retained safely.
    
* Properly configuring ingress and TLS certificates is essential for secure access.
    
* Redis and PostgreSQL are critical for Harbor functionality and must be monitored.
    
* Docker login, build, and push are straightforward once Harbor is correctly configured.
    

Deploying Harbor with Helm on Kubernetes is straightforward if you follow these steps carefully. With persistence, security, and a proper workflow, Harbor becomes a reliable, enterprise-ready registry for your container images.
