Self-Host Harbor Registry with Docker Compose, TLS, and Trivy

Updated on Aug 22, 2026
Mila H
10 MINS READ
Table of Contents
Self-Host Harbor Container Registry

Docker Hub's rate limits and basic GitLab registries push many DevOps engineers to self-host Harbor container registry instead. This guide shows you how to deploy Harbor with Docker Compose, secured with TLS, persistent storage, RBAC, and image replication. Also, you will learn how to enable Trivy scanning and integrate it with your Gitea CI/CD pipeline.

What Is Harbor and Why Self-Host It

Harbor is a CNCF, open-source registry that adds a web UI, RBAC, image replication, audit logs, and built-in Trivy scanning on top of plain Docker Registry. Choosing to self-host Harbor container registry gives you full control over storage, network, and compliance with no SaaS fees.

Unlike Docker Hub's rate limits or a basic GitLab registry, Harbor offers per-project permissions, automatic CVE scanning on every push, and native cross-region replication.

Prerequisites to Self-Host Harbor Container Registry

Before you begin to self-host Harbor container registry, make sure your server meets these requirements:

  • A Linux host running Ubuntu 24.04 or newer with at least 4 vCPUs, 8 GB RAM, and 40 GB of free disk space.
  • Root or sudo access to the server.
  • A registered domain name pointing to the server's public IP.

For heavy compute, a dedicated server with NVMe storage keeps image pull and push times fast, especially with multiple CI runners hitting the registry at once.

Step 1. Update the Server and Install Docker

The first step is to install Docker and Docker Compose on your server:

Bash
sudo apt update && sudo apt upgrade -ysudo apt install ca-certificates curl gnupg lsb-release -y # Add Docker's official GPG keysudo install -m 0755 -d /etc/apt/keyringscurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpgsudo chmod a+r /etc/apt/keyrings/docker.gpg # Add the Docker repositoryecho \  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt updatesudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y # Verify installationdocker --versiondocker compose version

Add your user to the docker group so you don't need sudo for every command:

Bash
sudo usermod -aG docker $USERnewgrp docker

Step 2. Download the Harbor Installer

Harbor ships an offline installer bundle that generates the docker-compose.yml file for you based on your configuration. This is the officially supported way to self-host Harbor container registry with Docker Compose.

To download the Harbor installer, run the commands below:

Bash
cd /optsudo mkdir harbor-install && sudo chown $USER:$USER harbor-installcd harbor-install HARBOR_VERSION="v2.15.2"wget https://github.com/goharbor/harbor/releases/download/${HARBOR_VERSION}/harbor-offline-installer-${HARBOR_VERSION}.tgztar xzvf harbor-offline-installer-${HARBOR_VERSION}.tgzcd harbor

Always check the Harbor releases page for the latest stable tag.

Step 3. Generate TLS Certificates

Production deployments should never run over plain HTTP. If you already own a certificate from Let's Encrypt or another CA, skip straight to placing the files.

If you don't have one, generate a self-signed CA and server certificate with OpenSSL:

Bash
mkdir -p /data/certcd /data/cert # 1. Generate a CA private key and certificateopenssl genrsa -out ca.key 4096openssl req -x509 -new -nodes -sha512 -days 3650 \  -subj "/C=AE/ST=Dubai/L=Dubai/O=PerLod/OU=Infra/CN=PerLod Root CA" \  -key ca.key -out ca.crt # 2. Generate the server private key and CSRopenssl genrsa -out harbor.yourdomain.com.key 4096openssl req -sha512 -new \  -subj "/C=AE/ST=Dubai/L=Dubai/O=PerLod/OU=Infra/CN=harbor.yourdomain.com" \  -key harbor.yourdomain.com.key -out harbor.yourdomain.com.csr # 3. Create the x509 v3 extension file (required for SAN)cat > v3.ext <<-EOFauthorityKeyIdentifier=keyid,issuerbasicConstraints=CA:FALSEkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEnciphermentextendedKeyUsage = serverAuthsubjectAltName = @alt_names [alt_names]DNS.1=harbor.yourdomain.comEOF # 4. Sign the certificate with your CAopenssl x509 -req -sha512 -days 3650 \  -extfile v3.ext -CA ca.crt -CAkey ca.key -CAcreateserial \  -in harbor.yourdomain.com.csr -out harbor.yourdomain.com.crt

If you use a Let's Encrypt certificate instead, run Certbot in standalone mode and point harbor.yml at the fullchain.pem and privkey.pem files it generates. This avoids trust warnings entirely.

Also, make the Docker daemon trust the certificate too, so docker login and docker push work without --insecure-registry:

Bash
openssl x509 -inform PEM -in harbor.yourdomain.com.crt -out harbor.yourdomain.com.certsudo mkdir -p /etc/docker/certs.d/harbor.yourdomain.comsudo cp harbor.yourdomain.com.cert /etc/docker/certs.d/harbor.yourdomain.com/sudo cp harbor.yourdomain.com.key /etc/docker/certs.d/harbor.yourdomain.com/sudo cp ca.crt /etc/docker/certs.d/harbor.yourdomain.com/sudo systemctl restart docker

Step 4. Set Up harbor.yml for Production

From the harbor directory, copy the template configuration file and edit it. This is the central file that controls how you self-host Harbor container registry, including hostname, TLS paths, admin password, database, and Trivy settings:

Bash
cd harborcp harbor.yml.tmpl harbor.ymlnano harbor.yml

Update the following key fields:

YAML
hostname: harbor.yourdomain.com http:  port: 80 https:  port: 443  certificate: /data/cert/harbor.yourdomain.com.crt  private_key: /data/cert/harbor.yourdomain.com.key harbor_admin_password: ChangeThisStrongPassword123! database:  password: ChangeThisDbPasswordToo123!  max_idle_conns: 100  max_open_conns: 900 data_volume: /data trivy:  ignore_unfixed: false  skip_update: false  insecure: false jobservice:  max_job_workers: 10 log:  level: info  local:    rotate_count: 50    rotate_size: 200M    location: /var/log/harbor

The data_volume path is where all persistent storage for images, the database, and Redis lives on disk, so make sure that path sits on your fastest and largest disk. This is the most important setting for anyone planning to self-host Harbor container registry at scale.

Step 5. Run the Harbor Installer with Trivy Enabled

Run the installer with the --with-trivy flag so the built-in vulnerability scanner is deployed as an additional container alongside Harbor's core services:

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

The script will validate your configuration and generate the final docker-compose.yml from harbor.yml. It pulls all required images, creates the Docker network and named volumes for persistent storage, and starts every Harbor component, including:

Bash
core, portal, jobservice, registry, registryctl, nginx, redis, postgresql, and trivy-adapter.

Run the Harbor Installer with Trivy

Confirm every container is healthy:

Bash
docker compose psdocker compose logs -f core

At this point, you have a working environment on which to self-host Harbor container registry. Reach it from your browser:

Bash
https://harbor.yourdomain.com

Log in with username admin and the password you set in harbor_admin_password.

Harbor log in

You should see the Harbor dashboard:

Harbor dashboard

Step 6. Verify TLS and Persistent Storage

Confirm the certificate is being served correctly with the commands below:

Bash
curl -v https://harbor.yourdomain.com 2>&1 | grep -i "SSL certificate"docker login harbor.yourdomain.com

Check that data actually persists across container restarts, which matters a lot once you self-host Harbor container registry for real workloads rather than a quick test:

Bash
docker compose downdocker compose up -ddocker compose ps

Your images, projects, and users should still be present after this restart cycle because everything is stored under /data on the host, not inside the containers.

Step 7. Configure Project-Based RBAC

Harbor organizes images into projects, and each project has its own access control list. From the web UI:

Go to Projects > New Project, name it, for example, production, and choose Private unless you want public pulls.

  • Project Name: production
  • Access Level > Public: leave this unchecked
  • Project quota limits: leave -1 (unlimited)
  • Proxy Cache: leave off

Configure Harbor Project-Based RBAC

Open the project, go to the Members tab, and click + User to assign roles

  • Name: type the exact Harbor username of the person you want to add. They must already have a Harbor account; go to Administration > Users first if they don't exist yet.
  • Role: pick based on what that person should do:
    • Developer: for most engineers who just need to push/pull images.
    • Maintainer: for leads who also need to trigger scans or tweak project config.
    • Guest: read-only, pull access only.
    • Limited Guest: read-only but can't see logs.
    • Project Admin: full control; use carefully only for you or another admin.

Add members to harbor project

Repeat for every team or environment that needs isolated access.

This project-based RBAC is a big reason teams self-host Harbor container registry instead of using a flat namespace like Docker Hub, where per-image permissions are much harder to enforce.

Note: For automated systems such as CI/CD pipelines, create a Robot Account instead of using a human account:

Bash
Projectsyour-projectRobot AccountsNew Robot Account

Grant it only push and pull permissions scoped to that project, then copy the generated token immediately; Harbor shows it only once.

Step 8. Set Up Image Replication Rules

Replication lets Harbor automatically copy images to or from another registry, which is useful for multi-region deployments or disaster recovery.

  • Go to Administration > Registries > New Endpoint, and enter the target registry's URL, provider type, and credentials.
  • Go to Administration > Replications > New Replication Rule.
  • Choose the source project, the destination endpoint, and a trigger mode: Manual, Scheduled (cron), or Event-Based (triggers automatically on every push).
  • Optionally filter by repository name pattern or tag pattern so only specific images replicate.

Event-based replication is the most common choice when you self-host Harbor container registry across two data centers; it keeps a backup copy in sync automatically.

Step 9. Verify Trivy Vulnerability Scanning

Because you installed Harbor with --with-trivy, the scanner is already registered. Verify it and turn on automatic scanning:

Log in as admin, go to Administration > Interrogation Services > Scanners, and confirm Trivy shows a Healthy status.

Verify Trivy Vulnerability Scanning is healthy in Harbor

  • Open any project, go to its Configuration tab, and enable Automatically scan images on push so every new image is scanned the moment it lands in the registry.
  • Optionally enable Prevent vulnerable images from running and set a severity threshold, for example, block anything rated Critical, to stop compromised images from ever being pulled.
  • To scan everything already stored, go to Administration > Interrogation Services > Vulnerability, click Scan Now, or set up a recurring schedule with Schedule to scan all.

Interrogation services vulnerability scan

Push a test image and check its scan report to confirm the pipeline works correctly:

Bash
docker pull alpine:3.19docker tag alpine:3.19 harbor.yourdomain.com/production/alpine:3.19docker push harbor.yourdomain.com/production/alpine:3.19

Open the image in the Harbor UI under Projects > production > Repositories and click the tag to view its Trivy CVE report, broken down by severity.

Step 10. Connect Harbor to Your Gitea CI/CD Pipeline

Now that Harbor is running with TLS, RBAC, and scanning, you can connect it as the private registry target for your Gitea Actions or Gitea CI pipeline. If you haven't set up Gitea's CI/CD yet, follow our Gitea self-hosted CI/CD with Docker Compose guide first, then come back to finish this integration.

In Gitea, go to the repository's Settings > Secrets, and add HARBOR_USERNAME and HARBOR_PASSWORD (use the robot account token you created in Step 7, not the admin password).

In your Gitea Actions workflow file (.gitea/workflows/build.yml), add a login and push step:

YAML
name: build-and-pushon: [push]jobs:  build:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - name: Log in to Harbor        run: echo "${{ secrets.HARBOR_PASSWORD }}" | docker login harbor.yourdomain.com -u "${{ secrets.HARBOR_USERNAME }}" --password-stdin      - name: Build image        run: docker build -t harbor.yourdomain.com/production/myapp:${{ github.sha }} .      - name: Push image        run: docker push harbor.yourdomain.com/production/myapp:${{ github.sha }}

Every push now triggers a build, a push to Harbor, and an automatic Trivy scan, giving you a complete build-scan-store loop without touching Docker Hub.

With this in place, Gitea handles source control and automation, while Harbor is the trusted, scanned image store your pipeline pushes to, and your servers pull from.

Harbor Production Hardening Checklist

Before you consider this deployment done, review this checklist:

  • Rotate harbor_admin_password and database passwords immediately after first login.
  • Use a real CA-signed certificate instead of a self-signed one for anything internet-facing.
  • Put Harbor's /data directory on fast NVMe storage.
  • Enable Harbor's built-in garbage collection on a schedule to reclaim space from deleted image layers.
  • Back up the PostgreSQL volume and /data/cert directory regularly; losing the certificate breaks every client that pinned the CA.
  • Restrict inbound access to the Harbor host with a firewall or VPN if it doesn't need to be publicly reachable.

If you're planning to self-host Harbor container registry for a team, disk and network speed matter most, because Harbor is I/O-heavy, and Trivy adds extra load. A PerLod dedicated server with NVMe storage keeps pull/push times low under heavy CI traffic, with full root access to tune everything yourself.

Conclusion

At this point, you have learned to self-host Harbor container registry with Docker Compose, TLS, persistent storage, RBAC, replication, and Trivy scanning. Connected to Gitea CI/CD, Harbor becomes your team's single source of truth for images, a private, secure registry you fully control instead of Docker Hub.

We hope you enjoy this guide.

Yes, Trivy is optional, but enabling it costs nothing and adds automated vulnerability scanning on every image.

It depends on image count and size, but plan for at least 40 to 100 GB to start, and monitor growth since every pushed layer and its scan cache consume storage.

Yes, use Harbor's replication feature to pull images from Docker Hub into a project, or docker pull, retag, and docker push them manually.