How to Self-Host Harbor Container Registry with Docker Compose, TLS, and Trivy Vulnerability Scanning
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, secure it with TLS, add persistent storage, set up RBAC, configure image replication, enable Trivy scanning, and connect it to your Gitea CI/CD pipeline.
Table of Contents
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:
sudo apt update && sudo apt upgrade -y
sudo apt install ca-certificates curl gnupg lsb-release -y
# Add Docker's official GPG key
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
# Add the Docker repository
echo \
"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 update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
# Verify installation
docker --version
docker compose version
Add your user to the docker group so you don’t need sudo for every command:
sudo usermod -aG docker $USER
newgrp 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:
cd /opt
sudo mkdir harbor-install && sudo chown $USER:$USER harbor-install
cd harbor-install
HARBOR_VERSION="v2.15.2"
wget https://github.com/goharbor/harbor/releases/download/${HARBOR_VERSION}/harbor-offline-installer-${HARBOR_VERSION}.tgz
tar xzvf harbor-offline-installer-${HARBOR_VERSION}.tgz
cd 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:
mkdir -p /data/cert
cd /data/cert
# 1. Generate a CA private key and certificate
openssl genrsa -out ca.key 4096
openssl 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 CSR
openssl genrsa -out harbor.yourdomain.com.key 4096
openssl 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 <<-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
# 4. Sign the certificate with your CA
openssl 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:
openssl x509 -inform PEM -in harbor.yourdomain.com.crt -out harbor.yourdomain.com.cert
sudo mkdir -p /etc/docker/certs.d/harbor.yourdomain.com
sudo 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:
cd harbor
cp harbor.yml.tmpl harbor.yml
nano harbor.yml
Update the following key fields:
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:
sudo ./install.sh --with-trivy
The script will validate your configuration, generate the final docker-compose.yml from harbor.yml, pull all required images, create the Docker network and named volumes for persistent storage, and start every Harbor component, including:
core, portal, jobservice, registry, registryctl, nginx, redis, postgresql, and trivy-adapter.

Confirm every container is healthy:
docker compose ps
docker 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:
https://harbor.yourdomain.com
Log in with username admin and the password you set in harbor_admin_password.

You should see the Harbor dashboard:

Step 6. Verify TLS and Persistent Storage
Confirm the certificate is being served correctly with the commands below:
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:
docker compose down
docker compose up -d
docker 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

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.

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:
Projects → your-project → Robot Accounts → New 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.

- 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.

Push a test image and check its scan report to confirm the pipeline works correctly:
docker pull alpine:3.19
docker tag alpine:3.19 harbor.yourdomain.com/production/alpine:3.19
docker 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:
name: build-and-push
on: [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_passwordand 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
/datadirectory 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/certdirectory 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.
FAQs
Can I run Harbor without Trivy?
Yes, Trivy is optional, but enabling it costs nothing and adds automated vulnerability scanning on every image.
How much disk space does Harbor need?
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.
Can I migrate existing Docker Hub images into Harbor?
Yes, use Harbor’s replication feature to pull images from Docker Hub into a project, or docker pull, retag, and docker push them manually.