Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Harbor Ubuntu
GUIDEInstall Guides

How to Install Harbor on Ubuntu 24.04 VPS: Self-Hosted Container Image Registry with Scanning

31 min read

How to Install Harbor on Ubuntu 24.04 VPS: Self-Hosted Container Image Registry with Scanning

Container images are the new application artifact. Whether you ship microservices to Kubernetes, build GitOps pipelines, or package internal tools for your team, you need somewhere reliable to store and distribute those images. Docker Hub's public tier throttles anonymous pulls to 100 per six hours, team plans start at USD 11 per user per month, and AWS ECR charges USD 0.10 per GB-month of storage plus egress fees that add up fast once your nodes start pulling images hundreds of times a day. This guide walks you through installing Harbor, the CNCF-graduated open-source registry, on a single Ubuntu 24.04 VPS with Trivy vulnerability scanning, replication, webhook notifications, and optional S3/MinIO object storage.

By the end you will have a hardened, HTTPS-protected registry at harbor.yourdomain.com that your developers and CI/CD runners can docker push and docker pull against exactly like Docker Hub or ECR -- except you own the data, there are no egress fees, and every image is automatically scanned for CVEs.

Skip the manual setup? Deploy a Harbor-ready VPS from our CloudCore Professional plan with Docker Engine pre-installed for EUR 19.99/month.

Table of Contents

  • What is Harbor?
  • Why Self-Host a Registry vs Docker Hub or ECR?
  • Prerequisites
  • Step 1: Prepare the Ubuntu 24.04 Server
  • Step 2: Install Docker Engine and Docker Compose
  • Step 3: Obtain TLS Certificates
  • Step 4: Download the Harbor Offline Installer
  • Step 5: Configure harbor.yml
  • Step 6: Run install.sh with Trivy and Notary
  • Step 7: First Login and Push Test
  • Step 8: Create Projects and Robot Accounts
  • Step 9: Configure Replication Rules
  • Step 10: Enable Trivy Vulnerability Scanning
  • Step 11: Webhook Notifications
  • Step 12: Garbage Collection
  • Step 13: Use S3 or MinIO as the Storage Backend
  • Step 14: Upgrade Harbor
  • Troubleshooting
  • FAQ
  • Next Steps
  • What is Harbor?

    Harbor is an open-source cloud-native registry that stores, signs, and scans container images and Helm charts. Originally created at VMware and donated to the Cloud Native Computing Foundation, it became a CNCF Graduated project in 2020 alongside Kubernetes, Prometheus, and Envoy -- the highest maturity tier in the CNCF landscape.

    At its core Harbor is a collection of Docker-Compose-orchestrated services: the upstream Docker distribution registry for blob storage, a PostgreSQL database for metadata, Redis for job state, an Nginx proxy for TLS termination, a core API server, a job service for async tasks like replication and garbage collection, and optional components like Trivy for vulnerability scanning and Notary for image signing.

    What makes Harbor different from a plain docker registry:2 container is the feature surface around the registry:

    • Multi-tenancy with projects -- each project is an isolated namespace with its own access policies, quotas, and replication rules.
    • Role-based access control (RBAC) -- project admins, developers, maintainers, and limited guests map to standard team workflows.
    • Robot accounts -- long-lived tokens for CI/CD and Kubernetes image pulls that are scoped per-project and can be rotated independently of human users.
    • Vulnerability scanning -- Trivy scans every pushed image against the Aqua Security vulnerability database and surfaces CVEs in the UI and API.
    • Replication -- mirror repositories to or from Docker Hub, GitHub Container Registry (GHCR), quay.io, AWS ECR, Azure ACR, Google GCR, and other Harbor instances.
    • Webhooks -- fire HTTP POSTs to Slack, Discord, Jenkins, ArgoCD, or your own API when images are pushed, scanned, replicated, or deleted.
    • Tag retention and immutability -- policies that keep the last N tags of each repository and prevent accidental overwrites.
    • OCI artifact support -- stores not just Docker images but Helm 3 charts, OPA bundles, WebAssembly modules, and any OCI artifact.
    • Image signing via Cosign/Notary -- cryptographically sign images and enforce signed-only pull policies.
    In practice Harbor replaces three or four separate tools (registry + scanner + mirror + signing) with one cohesive UI and API.

    Why Self-Host a Registry vs Docker Hub or ECR?

    Moving your container registry onto your own VPS is one of the highest-leverage infra decisions a small-to-medium team can make. The economics are compelling, and the operational benefits compound over time.

    Cost Comparison: Harbor vs Docker Hub vs AWS ECR

    ScenarioDocker Hub TeamAWS ECRSelf-Hosted Harbor
    Monthly base costUSD 11/user/mo (5 users = USD 55)USD 0 (pay as you go)EUR 19.99/mo flat
    Storage costIncludedUSD 0.10 / GB-monthIncluded in VPS disk
    Pull bandwidth (500 GB/mo)Included~USD 45/mo egressIncluded (unmetered)
    Private image scanningPaid add-onUSD 0.09 per scan (basic free)Included (Trivy)
    Pull rate limits200 pulls / 6hr (team)10,000 req/sec per accountNone
    Replication to other registriesNot supportedCross-Region Replication (paid)Included
    Webhook integrationsLimitedEventBridge (extra setup)Native, unlimited
    Data sovereigntyUS onlyPer-regionFull control
    Image signingPaid (DCT deprecated)ECR Signer (extra tooling)Included (Notary/Cosign)
    For a team of five developers running CI pipelines that push and pull a few hundred images per day with ~200 GB of image storage, Harbor on a EUR 20/month VPS typically replaces USD 100-250/month of combined Docker Hub + ECR spend. Beyond cost, the most valuable reasons teams self-host are:

    • No rate limits on pulls -- GitOps agents like ArgoCD or Flux can re-pull images on every reconcile loop without tripping quotas.
    • Data sovereignty -- images containing proprietary binaries, licensed software, or regulated workloads stay on infrastructure you control.
    • Air-gapped and offline workflows -- deploy Harbor into a private network with no internet access and still run a full CI/CD pipeline.
    • Custom retention and compliance -- keep images for the 7-year retention your auditor requires, or purge them in 30 days, with per-project rules.
    • Unified UI for humans and pipelines -- no more juggling three different registry web consoles for different environments.

    Prerequisites

    Before you start, confirm you have:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access.
    • A domain name (for example harbor.yourdomain.com) with an A record pointing to the VPS public IP.
    • Ports 80 and 443 open in your firewall / cloud provider security group.
    • At least 4 vCPU, 8 GB RAM, and 60 GB of disk -- Trivy adds ~2 GB of RAM overhead and the vulnerability database itself is ~1 GB.
    • Docker Engine 20.10+ and Docker Compose v2 (we install both below).
    • OpenSSL (pre-installed on Ubuntu).
    Recommended Plan: CloudCore Professional
    >
    Harbor runs comfortably on the CloudCore Professional plan:
    >
    - 6 vCPU cores
    - 12 GB RAM
    - 100 GB NVMe SSD
    - Unmetered bandwidth
    - EUR 19.99/month
    >
    This gives you headroom for Harbor + Trivy + Redis + PostgreSQL + your largest concurrent pushes. For teams storing more than 200 GB of images, pair it with block storage or an S3 backend (see Step 13).

    If you have not already set up Docker on the host, start with our Docker install guide -- Step 2 below mirrors its key commands.

    Connect to your server:

    bash
    ssh root@your-server-ip

    Step 1: Prepare the Ubuntu 24.04 Server

    Update package lists and upgrade existing packages:

    bash
    sudo apt update && sudo apt upgrade -y

    Install the baseline utilities we will need (curl, git, openssl, vim, and the UFW firewall):

    bash
    sudo apt install -y curl git openssl vim ufw

    Open the ports Harbor uses and enable the firewall:

    bash
    sudo ufw allow 22/tcp
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw --force enable
    sudo ufw status

    Expected output:

    text
    Status: active
    To                         Action      From
    --                         ------      ----
    22/tcp                     ALLOW       Anywhere
    80/tcp                     ALLOW       Anywhere
    443/tcp                    ALLOW       Anywhere

    Set a hostname that matches the DNS record:

    bash
    sudo hostnamectl set-hostname harbor.yourdomain.com

    Step 2: Install Docker Engine and Docker Compose

    Harbor ships as a set of Docker Compose services, so we need a working Docker Engine + Compose v2 plugin.

    Add Docker's official repository and install:

    bash
    sudo apt install -y ca-certificates gnupg lsb-release
    sudo install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    sudo chmod a+r /etc/apt/keyrings/docker.gpg

    echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

    sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

    Confirm the versions (Harbor 2.10+ requires Docker 20.10+ and Compose v2):

    bash
    docker --version
    docker compose version

    Expected output:

    text
    Docker version 26.1.3, build b72abbb
    Docker Compose version v2.27.0

    Enable Docker at boot and add your user to the docker group (log out and back in for it to take effect):

    bash
    sudo systemctl enable --now docker
    sudo usermod -aG docker "$USER"

    For a more detailed walkthrough including post-install hardening, see our Docker Ubuntu install guide.

    Step 3: Obtain TLS Certificates

    Harbor strongly recommends HTTPS -- the Docker CLI refuses to push to plain HTTP registries by default, and disabling that safeguard cluster-wide is a non-starter for real teams.

    Option A: Let's Encrypt via Certbot (Recommended)

    If harbor.yourdomain.com resolves publicly to your VPS, grab a free certificate:

    bash
    sudo apt install -y certbot
    sudo certbot certonly --standalone -d harbor.yourdomain.com \
      --non-interactive --agree-tos -m [email protected]

    Certbot writes certs to /etc/letsencrypt/live/harbor.yourdomain.com/. Copy them to a location Harbor will reference:

    bash
    sudo mkdir -p /data/cert
    sudo cp /etc/letsencrypt/live/harbor.yourdomain.com/fullchain.pem /data/cert/harbor.crt
    sudo cp /etc/letsencrypt/live/harbor.yourdomain.com/privkey.pem /data/cert/harbor.key
    sudo chmod 644 /data/cert/harbor.crt
    sudo chmod 600 /data/cert/harbor.key

    Set up auto-renewal with a deploy hook that restarts Harbor after renewal:

    bash
    sudo tee /etc/letsencrypt/renewal-hooks/deploy/harbor.sh > /dev/null <<'EOF'
    #!/bin/bash
    cp /etc/letsencrypt/live/harbor.yourdomain.com/fullchain.pem /data/cert/harbor.crt
    cp /etc/letsencrypt/live/harbor.yourdomain.com/privkey.pem /data/cert/harbor.key
    cd /opt/harbor && docker compose restart proxy
    EOF
    sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/harbor.sh

    Option B: Self-Signed Certificate (Internal / Dev)

    For an internal registry without public DNS, generate a self-signed cert:

    bash
    sudo mkdir -p /data/cert && cd /data/cert

    sudo openssl genrsa -out ca.key 4096 sudo openssl req -x509 -new -nodes -sha512 -days 3650 \ -subj "/C=US/ST=CA/L=SF/O=example/OU=IT/CN=harbor-ca" \ -key ca.key -out ca.crt

    sudo openssl genrsa -out harbor.key 4096 sudo openssl req -sha512 -new \ -subj "/C=US/ST=CA/L=SF/O=example/OU=IT/CN=harbor.yourdomain.com" \ -key harbor.key -out harbor.csr

    sudo tee v3.ext > /dev/null <<EOF authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] DNS.1=harbor.yourdomain.com EOF

    sudo openssl x509 -req -sha512 -days 3650 \ -extfile v3.ext \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -in harbor.csr -out harbor.crt

    Clients pulling from a self-signed Harbor must trust ca.crt -- copy it into /etc/docker/certs.d/harbor.yourdomain.com/ca.crt on every Docker host that needs access.

    Step 4: Download the Harbor Offline Installer

    Harbor offers two installer flavors: online (pulls images at install time) and offline (bundles all images in the tarball). We use the offline installer because it is reproducible and works on air-gapped hosts.

    Check the latest release and download it:

    bash
    cd /opt
    sudo curl -L -O https://github.com/goharbor/harbor/releases/download/v2.11.1/harbor-offline-installer-v2.11.1.tgz
    sudo tar xzvf harbor-offline-installer-v2.11.1.tgz
    cd harbor
    ls -la

    Expected contents:

    text
    common.sh
    harbor.v2.11.1.tar.gz
    harbor.yml.tmpl
    install.sh
    LICENSE
    prepare

    The harbor.v2.11.1.tar.gz is the bundle of container images; install.sh loads them into Docker and then runs docker compose up.

    Step 5: Configure harbor.yml

    Copy the template to harbor.yml, then edit it:

    bash
    sudo cp harbor.yml.tmpl harbor.yml
    sudo vim harbor.yml

    Key settings to change (leave others at default unless noted):

    yaml
    # The FQDN clients will use to reach Harbor
    hostname: harbor.yourdomain.com

    Plain HTTP listener (kept on 80 for Let's Encrypt HTTP-01 redirects)

    http: port: 80

    HTTPS listener with the certs from Step 3

    https: port: 443 certificate: /data/cert/harbor.crt private_key: /data/cert/harbor.key

    Admin password for first UI login (change this!)

    harbor_admin_password: ChangeMeToAStrongPassword

    PostgreSQL password

    database: password: root123 max_idle_conns: 100 max_open_conns: 900

    Where Harbor stores images, DB, and logs on the host

    data_volume: /data

    Log rotation

    log: level: info local: rotate_count: 50 rotate_size: 200M location: /var/log/harbor

    Scheduled jobs (garbage collection, scans, replication)

    jobservice: max_job_workers: 10

    Notifications

    notification: webhook_job_max_retry: 3 webhook_job_http_client_timeout: 3

    Save and exit. A couple of notes:

    • data_volume: /data is where all persistent state lives. Back up this directory and the PostgreSQL volume together when you snapshot.
    • harbor_admin_password is only used during the first install.sh run. After install, change it in the UI or via the API -- editing harbor.yml later has no effect on existing admin accounts.
    • If you are using S3 or MinIO for blob storage, see Step 13 before running install.sh -- the storage backend cannot easily be swapped after install without a data migration.

    Step 6: Run install.sh with Trivy and Notary

    The installer accepts feature flags. For a modern stack you want Trivy (vulnerability scanning) and optionally Notary (legacy image signing; Cosign is the modern alternative but Harbor still ships Notary):

    bash
    sudo ./install.sh --with-trivy --with-notary

    Note: Harbor 2.11+ has deprecated Notary in favor of Cosign verification. If you are on Harbor 2.12+, drop --with-notary and use Cosign instead (Harbor has native Cosign signature viewing in the UI). For this guide we show the full historical flag set:

    bash
    sudo ./install.sh --with-trivy

    Expected output (abbreviated):

    text
    [Step 0]: checking if docker is installed ...
    Note: docker version: 26.1.3
    [Step 1]: checking docker-compose is installed ...
    Note: Docker Compose version v2.27.0
    [Step 2]: loading Harbor images ...
    Loaded image: goharbor/redis-photon:v2.11.1
    Loaded image: goharbor/harbor-portal:v2.11.1
    Loaded image: goharbor/harbor-core:v2.11.1
    Loaded image: goharbor/harbor-jobservice:v2.11.1
    Loaded image: goharbor/registry-photon:v2.11.1
    Loaded image: goharbor/nginx-photon:v2.11.1
    Loaded image: goharbor/trivy-adapter-photon:v2.11.1
    ...
    [Step 3]: preparing environment ...
    [Step 4]: preparing harbor configs ...
    ...
    [Step 5]: starting Harbor ...
    [+] Running 11/11
     Container harbor-log         Started
     Container registry           Started
     Container redis              Started
     Container harbor-portal      Started
     Container registryctl        Started
     Container harbor-db          Started
     Container harbor-core        Started
     Container trivy-adapter      Started
     Container harbor-jobservice  Started
     Container nginx              Started

    --- Harbor has been installed and started successfully.----

    Confirm the stack is healthy:

    bash
    sudo docker compose ps

    Every container should show Up (healthy). Browse to https://harbor.yourdomain.com -- you will see the Harbor login screen.

    Step 7: First Login and Push Test

    Log in with username admin and the password you set in harbor.yml.

    From any Docker client machine, log in to your new registry:

    bash
    docker login harbor.yourdomain.com -u admin

    Enter the admin password when prompted. Then push a test image:

    bash
    docker pull nginx:1.27-alpine
    docker tag nginx:1.27-alpine harbor.yourdomain.com/library/nginx:1.27-alpine
    docker push harbor.yourdomain.com/library/nginx:1.27-alpine

    Expected output:

    text
    The push refers to repository [harbor.yourdomain.com/library/nginx]
    a8b77d40d045: Pushed
    4dcab49015d4: Pushed
    ...
    1.27-alpine: digest: sha256:abcd1234... size: 1362

    Refresh the Harbor UI: under Projects > library > Repositories you should see nginx with one artifact.

    Step 8: Create Projects and Robot Accounts

    Harbor organizes images into projects. The default library project is public; real work should go into private projects with scoped RBAC.

    Create a Private Project

    In the UI: Projects > + New Project.

    • Project Name: production
    • Access Level: Private (clear the Public checkbox)
    • Storage quota: set a per-project limit (for example 50 GB) to prevent runaway usage
    Click OK. You can also create projects via the API:

    bash
    curl -u admin:ChangeMeToAStrongPassword -X POST \
      https://harbor.yourdomain.com/api/v2.0/projects \
      -H "Content-Type: application/json" \
      -d '{
        "project_name": "production",
        "metadata": {"public": "false"},
        "storage_limit": 53687091200
      }'

    Create a Robot Account for CI/CD

    Robot accounts are long-lived tokens scoped to one project -- perfect for Kubernetes imagePullSecret or GitHub Actions.

    Navigate to Projects > production > Robot Accounts > + New Robot Account.

    • Name: gha-ci
    • Expires in: 365 days
    • Permissions: Push + Pull on Repository, Pull on Artifact
    On creation, Harbor displays the secret exactly once -- copy it immediately. The resulting username is robot$production+gha-ci.

    Use the robot token in GitHub Actions:

    yaml
    - name: Log in to Harbor
      uses: docker/login-action@v3
      with:
        registry: harbor.yourdomain.com
        username: ${{ secrets.HARBOR_ROBOT_USER }}
        password: ${{ secrets.HARBOR_ROBOT_TOKEN }}

    Or in Kubernetes -- this pairs well with a K3s cluster:

    bash
    kubectl create secret docker-registry harbor-pull \
      --namespace production \
      --docker-server=harbor.yourdomain.com \
      --docker-username='robot$production+gha-ci' \
      --docker-password='<paste-token-here>'

    kubectl patch serviceaccount default -n production \ -p '{"imagePullSecrets": [{"name": "harbor-pull"}]}'

    Step 9: Configure Replication Rules

    Replication mirrors images between registries on a schedule or on every push. Common use cases:

    • Pull-through cache for Docker Hub so your nodes never hit public rate limits.
    • Push to staging -> promote to production by replicating across two Harbor instances.
    • Backup by mirroring to a geographically separate Harbor or S3 bucket.

    Add an Upstream Registry (Docker Hub)

    Administration > Registries > + New Endpoint:

    • Provider: Docker Hub
    • Name: docker-hub-upstream
    • Access ID: your Docker Hub username
    • Access Secret: a Docker Hub personal access token
    • Test Connection, then save.

    Create a Pull-Through Replication Rule

    Administration > Replications > + New Replication Rule:

    • Name: mirror-nginx-from-dockerhub
    • Replication Mode: Pull-based
    • Source Registry: docker-hub-upstream
    • Source Resource Filter: name matches library/nginx, tag matches 1.27-*
    • Destination: library/nginx-mirror
    • Trigger Mode: Scheduled (cron 0 /6 for every 6 hours) or Event-Based
    Click Save. Harbor now pulls matching nginx:1.27-* tags from Docker Hub into your local registry every six hours. Kubernetes nodes that pull harbor.yourdomain.com/library/nginx-mirror:1.27.2 get the image from your VPS without ever hitting hub.docker.com.

    Push Replication to a Second Harbor

    If you run two Harbor instances (for example dev and prod), register the destination Harbor as an endpoint, then create a Push-based rule with Trigger Mode: Event-Based. Every time an image lands in production/* on dev, Harbor forwards it to prod immediately.

    Step 10: Enable Trivy Vulnerability Scanning

    If you ran install.sh with --with-trivy, the scanner is already running as the trivy-adapter container. The first run downloads the ~1 GB Aqua Trivy vulnerability database from GitHub, which takes 2-3 minutes.

    Trigger a Manual Scan

    Projects > production > Repositories > nginx > (click tag) > Scan.

    Within a few seconds Harbor shows a CVE breakdown: Critical / High / Medium / Low / Negligible counts with links to NVD entries for each vulnerability.

    Schedule Automatic Scans

    Administration > Interrogation Services > Vulnerability:

    • Scan All: Daily at 02:00
    • Auto-scan on push: enable at the project level (Projects > production > Configuration > Automatically scan images on push)

    Block Pulls of Vulnerable Images

    In Projects > production > Configuration:

    • Check Prevent vulnerable images from running
    • Severity: High (or Critical)
    Now any client attempting docker pull harbor.yourdomain.com/production/myapp:tag where the image has an unfixed High or Critical CVE will receive an authorization error. This turns Harbor into a supply-chain enforcement point -- developers can still push vulnerable images during builds, but they cannot promote or deploy them.

    Update the Trivy Database

    The Trivy container refreshes the DB every 12 hours automatically. To force an immediate refresh:

    bash
    sudo docker compose exec trivy-adapter /home/scanner/entrypoint.sh

    Or restart the container:

    bash
    sudo docker compose restart trivy-adapter

    Step 11: Webhook Notifications

    Webhooks let Harbor notify other systems when artifacts are pushed, pulled, deleted, scanned, or replicated.

    Projects > production > Webhooks > + New Webhook:

    • Name: slack-pushes
    • Event Type: select Artifact pushed, Scanning completed, Scanning failed
    • Notification Type: HTTP
    • Endpoint URL: https://hooks.slack.com/services/T000.../B000.../xxxx (or your own API)
    • Auth Header: optional bearer token
    • Verify Remote Certificate: on
    Harbor POSTs a JSON payload like this on each matching event:

    json
    {
      "type": "PUSH_ARTIFACT",
      "occur_at": 1713283200,
      "operator": "robot$production+gha-ci",
      "event_data": {
        "resources": [{
          "digest": "sha256:abcd1234...",
          "tag": "v1.2.3",
          "resource_url": "harbor.yourdomain.com/production/api:v1.2.3"
        }],
        "repository": {
          "name": "api",
          "namespace": "production",
          "repo_full_name": "production/api"
        }
      }
    }

    For Slack specifically, put a small translator in front of the webhook (or use a serverless function) to convert Harbor's payload into Slack's blocks format. You can also point webhooks at ArgoCD's image updater or Jenkins to trigger a redeploy on push.

    Step 12: Garbage Collection

    Deleting a tag in the Harbor UI marks the underlying blob as unreferenced, but does not immediately reclaim disk space. A garbage collection (GC) job must run to actually delete orphaned blobs from the registry storage backend.

    Run GC Manually

    Administration > Garbage Collection > + GC Now.

    Harbor puts the registry into read-only mode during GC (typically 30 seconds to 5 minutes depending on blob count). Clients attempting to push during this window receive a 503.

    Schedule Weekly GC

    On the same page:

    • Schedule: Weekly, Sunday at 03:00 UTC
    • Delete Untagged Artifacts: on
    • Dry Run: off
    GC logs are available under Administration > Garbage Collection > History. A typical weekly run on a 150 GB registry reclaims 5-15 GB.

    Pair GC with Tag Retention Rules

    Projects > production > Policy > Tag Retention > + Add Rule:

    • Matching Repository: **
    • Rule: retain the most recently pushed 20 artifacts
    • Except: tags matching prod- or v.. (keep release tags forever)
    Retention runs as a scheduled job and marks excess tags for deletion. The subsequent GC run is what actually frees space. Together they keep the registry from growing unbounded from CI-produced pr-123-abc1234 tags.

    Step 13: Use S3 or MinIO as the Storage Backend

    By default Harbor writes image blobs to /data/registry on the host. For production you probably want object storage for elastic capacity, built-in durability, and offsite backup. Harbor's underlying distribution registry supports S3, GCS, Azure Blob, Swift, and S3-compatible providers like MinIO, Backblaze B2, Wasabi, and Cloudflare R2.

    The cleanest path is to configure the backend before you first run install.sh. Edit harbor.yml and uncomment the storage_service block:

    yaml
    storage_service:
      ca_bundle:
      s3:
        region: us-east-1
        regionendpoint: https://minio.yourdomain.com
        bucket: harbor-registry
        accesskey: AKIA...
        secretkey: <secret>
        secure: true
        v4auth: true
        chunksize: 5242880
        rootdirectory: /
      redirect:
        disable: false

    If you are using self-hosted MinIO, the regionendpoint is your MinIO API URL and region can be any placeholder (MinIO ignores it). For AWS S3, omit regionendpoint and set region to the bucket region.

    Then run install.sh as usual. The registry container will write all new blobs to S3/MinIO instead of the local disk.

    Switching Storage Post-Install

    If you already installed Harbor with local storage and want to migrate, the process is:

  • Run a final GC to shrink the dataset.
  • docker compose down the stack.
  • rclone sync /data/registry s3-remote:harbor-registry to copy blobs to object storage.
  • Edit common/config/registry/config.yml (generated file) with the S3 stanza.
  • docker compose up -d.
  • Because this touches generated config it can break on the next install.sh upgrade. For that reason we strongly recommend configuring S3 before the first install.

    Cost Model

    Storing 500 GB of images on AWS S3 Standard runs ~USD 11.50/month + egress. On self-hosted MinIO using your own Contabo / Hetzner disks it is effectively free past the flat VPS cost. A common production pattern: Harbor on CloudCore Professional + MinIO on a second VPS with a large block volume, connected via private networking.

    Step 14: Upgrade Harbor

    Harbor ships a new minor release roughly every 3-4 months with security fixes. The upgrade process is straightforward because all state lives in /data and the PostgreSQL volume.

    Backup First

    bash
    cd /opt/harbor
    sudo docker compose down

    Snapshot the data directory

    sudo tar czf /root/harbor-data-$(date +%F).tar.gz /data

    Dump the PostgreSQL database

    sudo docker compose up -d postgresql sudo docker exec -t harbor-db pg_dumpall -U postgres > /root/harbor-db-$(date +%F).sql sudo docker compose down

    Install the New Release

    bash
    cd /opt
    sudo curl -L -O https://github.com/goharbor/harbor/releases/download/v2.12.0/harbor-offline-installer-v2.12.0.tgz
    sudo tar xzvf harbor-offline-installer-v2.12.0.tgz -C /opt/harbor-new --strip-components=1

    Copy your existing config into the new directory

    sudo cp /opt/harbor/harbor.yml /opt/harbor-new/harbor.yml

    Swap directories

    sudo mv /opt/harbor /opt/harbor-old sudo mv /opt/harbor-new /opt/harbor

    cd /opt/harbor sudo ./install.sh --with-trivy

    Harbor's built-in migrator detects the schema version on disk and runs any required PostgreSQL migrations automatically. Watch the output for DB schema upgrade completed before the stack comes up.

    Rollback

    If the upgrade fails:

    bash
    cd /opt
    sudo docker compose -f /opt/harbor/docker-compose.yml down
    sudo rm -rf /data && sudo tar xzf /root/harbor-data-YYYY-MM-DD.tar.gz -C /
    sudo mv /opt/harbor /opt/harbor-failed
    sudo mv /opt/harbor-old /opt/harbor
    cd /opt/harbor && sudo docker compose up -d

    Troubleshooting

    ProblemCauseSolution
    x509: certificate signed by unknown authority when pushingClient does not trust the self-signed CACopy ca.crt to /etc/docker/certs.d/harbor.yourdomain.com/ca.crt on the Docker host and restart Docker.
    unauthorized: authentication required after loginClock skew between client and HarborConfirm both sides run NTP / chronyd. Harbor JWT tokens are time-sensitive.
    http: server gave HTTP response to HTTPS clientProxy port mismatchCheck that https.port: 443 in harbor.yml and that your nginx proxy is not also listening on 443.
    Trivy scan stuck at QueuedDB download faileddocker compose logs trivy-adapter to see the error. Most often a proxy or GitHub rate limit. Restart the container once network is clean.
    Push fails with blob unknown to registryHarbor lost blob but kept manifest (disk full, interrupted GC)Run GC with Delete Untagged Artifacts enabled, then re-push the image.
    UI shows Internal Server Error after upgradeDatabase migration still runningdocker compose logs harbor-core -f until you see server listening on :8080.
    Out of disk space in /data/registryOld blobs not garbage collectedSet up the weekly GC from Step 12 and consider moving to MinIO.
    docker login fails with Error response from daemon: Get https://harbor...: dial tcp: i/o timeoutFirewall blocking 443 or DNS misconfigcurl -v https://harbor.yourdomain.com/v2/ from the client to isolate. Check UFW and cloud security groups.

    Reading Harbor Logs

    Logs for every component are in the harbor-log container's host mount /var/log/harbor/:

    bash
    sudo tail -f /var/log/harbor/core.log
    sudo tail -f /var/log/harbor/jobservice.log
    sudo tail -f /var/log/harbor/registry.log
    sudo tail -f /var/log/harbor/trivy-adapter.log

    Or stream via Docker Compose:

    bash
    cd /opt/harbor
    sudo docker compose logs -f harbor-core

    FAQ

    What is Harbor used for?

    Harbor is a self-hosted registry for container images, Helm charts, and other OCI artifacts. Teams use it to store private Docker images they do not want on Docker Hub, serve as a pull-through cache that dodges Docker Hub rate limits, run vulnerability scans with Trivy, enforce image signing, and replicate images between environments (dev/staging/prod) or cloud regions. In a typical Kubernetes + GitOps stack, Harbor sits between the CI system (which pushes images) and the cluster (which pulls them), enforcing policy at both ends.

    Is Harbor better than Docker Hub?

    It depends on your workload. Docker Hub is excellent for public open-source projects and individual developers -- the free tier is generous enough. Harbor wins when you have a team with private images, CI pipelines that push dozens of images per day, Kubernetes clusters pulling thousands of times per day, or compliance requirements that forbid your artifacts from leaving your infrastructure. Harbor also includes features that Docker Hub charges extra for (scanning, replication, webhooks, RBAC) and has no rate limits or per-user seat pricing.

    How much does it cost to run Harbor?

    The software itself is free (Apache 2.0 licensed). The running costs are the VPS plus any object storage. A single CloudCore Professional VPS at EUR 19.99/month comfortably handles 5-15 developers pushing images and a small Kubernetes cluster pulling them. Storing 100-200 GB of images locally on the NVMe is fine; past 200 GB consider adding MinIO or S3. For reference, the equivalent Docker Hub Team + AWS ECR + third-party scanner setup typically costs USD 100-300/month for the same workload.

    Can Harbor work with Kubernetes?

    Yes -- Harbor is Kubernetes-native. Create a robot account per namespace, generate a docker-registry pull secret with that robot token, and attach the secret to your namespace's default ServiceAccount. Every pod in that namespace then pulls from Harbor transparently. Harbor itself also has an official Helm chart if you want to run the registry inside a cluster rather than on a dedicated VPS, though the dedicated VPS pattern is simpler to operate. If you are standing up a cluster fresh, see the K3s install guide.

    Does Harbor support Helm charts?

    Yes. Harbor 2.x supports Helm 3 charts as OCI artifacts (the modern Helm chart format). Push with helm push mychart-0.1.0.tgz oci://harbor.yourdomain.com/production. The UI displays charts alongside container images in the same project. The older ChartMuseum-based Helm 2 support was removed in Harbor 2.8 -- use OCI Helm instead.

    How do I back up Harbor?

    Two things to back up: the data directory (/data) and the PostgreSQL database. For the data directory, any block-level or file-level backup tool works -- restic, borgbackup, or rsync to a remote target. For the database, docker exec harbor-db pg_dumpall -U postgres > harbor.sql on a schedule. Restore order: stop Harbor, restore /data, restore the database, start Harbor. Test this regularly; a backup you have never restored from is a hope, not a backup.

    What is the difference between Harbor and a plain Docker registry?

    The upstream registry:2 image is the bare blob store that Harbor wraps. It has no UI, no user accounts, no projects, no scanning, and no replication -- just a REST API for pushing and pulling manifests and blobs. Harbor adds the full product layer: web UI, PostgreSQL-backed user and permission model, Trivy integration, Helm chart support, replication engine, webhook dispatcher, garbage collection scheduler, and the job service that runs async tasks. If you have one developer and push ten images a year, registry:2 is fine. Once you have a team, you want Harbor.

    Next Steps

    Your Harbor instance is production-ready. To build on it:

    • Harden network access -- put Harbor behind Cloudflare Tunnel or a VPN for internal-only registries. See Cloudflared install guide.
    • Wire up GitOps -- point ArgoCD or FluxCD's image updater at your Harbor and let it auto-deploy new tags that pass scanning.
    • Sign images with Cosign -- cosign sign harbor.yourdomain.com/production/api:v1.0.0 then enforce cosign verify in an admission controller like Kyverno.
    • Add distributed storage -- stand up a MinIO cluster on a separate VPS and flip Harbor to S3 backend for elastic capacity. Walk through the MinIO install guide.
    • Run Harbor in Kubernetes -- deploy the official Harbor Helm chart on a K3s cluster when you outgrow single-host. See K3s install guide.
    • Export metrics to Prometheus -- Harbor exposes /metrics endpoints on every component. Scrape them with Prometheus and build a Grafana dashboard for storage growth, scan queue length, and push rate.
    • Read the upstream docs -- the official Harbor documentation covers advanced topics like LDAP integration, OIDC authentication, and multi-tenancy patterns.

    Run Harbor on a CloudCore Professional VPS
    >
    Harbor is happiest with fast NVMe storage, plenty of RAM for Trivy, and unmetered bandwidth for image pulls. Our CloudCore Professional plan checks every box:
    >
    - 6 vCPU cores
    - 12 GB RAM
    - 100 GB NVMe SSD (room for ~200 GB of deduplicated images after compression)
    - Unmetered bandwidth (no egress bills when your Kubernetes nodes pull images all day)
    - Ubuntu 24.04 LTS pre-installed
    - EUR 19.99/month flat -- no per-image, per-user, or per-pull fees
    >
    Get a CloudCore Professional VPS and have Harbor running in under an hour.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket