Harbor is an open source container registry from the CNCF. On top of storing images, it adds projects with role-based access control, vulnerability scanning with Trivy, robot accounts for automation, retention policies and replication to other registries. In this tutorial you will install Harbor on Ubuntu 24.04 with its online installer, secure it with a Let's Encrypt certificate, push and scan a first image, and create a robot account for your CI pipelines.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 2 vCPUs, 4 GB of RAM and 40 GB of disk. Harbor recommends 4 vCPUs, 8 GB of RAM and 160 GB of disk for production use; images take up most of the space.
- A non-root user with
sudoprivileges. - A domain name such as
registry.your_domainwith a DNS A record pointing to your server's public IP address. - Ports 80 and 443 reachable from the internet, and not used by another web server on this machine. Harbor runs its own Nginx container.
Replace registry.your_domain with your hostname throughout the guide.
Step 1 - Installing Docker and Docker Compose
Harbor runs as a set of containers managed by Docker Compose. Install Docker Engine and the Compose plugin:
sudo apt update
sudo apt install docker.io docker-compose-v2
sudo systemctl enable --now docker
Verify the installation:
sudo docker --version
sudo docker compose version
Docker version 27.x.x, build ...
Docker Compose version 2.x.x
Step 2 - Obtaining a TLS certificate
Docker clients require HTTPS for any registry that is not on localhost, so get the certificate before installing Harbor. Since nothing is listening on port 80 yet, use Certbot's standalone mode.
Open the firewall first. If UFW is not active, allow SSH before enabling it so you keep your session:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Install Certbot and request the certificate:
sudo apt install certbot
sudo certbot certonly --standalone -d registry.your_domain
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/registry.your_domain/fullchain.pem
Key is saved at: /etc/letsencrypt/live/registry.your_domain/privkey.pem
You will configure automatic renewal in Step 5, once Harbor is running on port 80.
Step 3 - Downloading and configuring Harbor
Check the latest stable release on the Harbor releases page and set it in a variable. The version below is an example; use the current one:
HARBOR_VERSION=v2.13.0
curl -fLO "https://github.com/goharbor/harbor/releases/download/${HARBOR_VERSION}/harbor-online-installer-${HARBOR_VERSION}.tgz"
Extract it to /opt, which creates /opt/harbor:
sudo tar -xzf "harbor-online-installer-${HARBOR_VERSION}.tgz" -C /opt
cd /opt/harbor
Create the configuration file from the template and open it:
sudo cp harbor.yml.tmpl harbor.yml
sudo nano harbor.yml
Edit the following keys. Leave the rest of the file as it is:
hostname: registry.your_domain
http:
port: 80
https:
port: 443
certificate: /etc/letsencrypt/live/registry.your_domain/fullchain.pem
private_key: /etc/letsencrypt/live/registry.your_domain/privkey.pem
harbor_admin_password: your_admin_password
database:
password: your_db_password
data_volume: /data
What each setting does:
hostnamemust be the exact name clients use to reach the registry. It is embedded in image references and tokens.- With
httpsenabled, requests to port 80 are redirected to HTTPS. harbor_admin_passwordis the initial password for theadminuser. Replace the template's defaultHarbor12345with a strong password. You can change it later in the UI.database.passwordis the password of Harbor's internal PostgreSQL database. Change the default here too.data_volumeis where images, the database and logs are stored on the host. Put it on your largest disk.
Protect the file, since it now contains passwords:
sudo chmod 600 harbor.yml
Step 4 - Installing Harbor with Trivy
Run the installer with the --with-trivy flag to include the Trivy vulnerability scanner. The installer checks Docker, generates the Compose configuration from harbor.yml, pulls the images and starts everything:
sudo ./install.sh --with-trivy
The last lines confirm a successful start:
[Step 5]: starting Harbor ...
...
----Harbor has been installed and started successfully.----
Check that all containers are running and healthy:
sudo docker compose ps
NAME IMAGE STATUS
harbor-core goharbor/harbor-core:v2.13.0 Up 1 minute (healthy)
harbor-db goharbor/harbor-db:v2.13.0 Up 1 minute (healthy)
harbor-jobservice goharbor/harbor-jobservice:v2.13.0 Up 1 minute (healthy)
harbor-log goharbor/harbor-log:v2.13.0 Up 1 minute (healthy)
harbor-portal goharbor/harbor-portal:v2.13.0 Up 1 minute (healthy)
nginx goharbor/nginx-photon:v2.13.0 Up 1 minute (healthy)
redis goharbor/redis-photon:v2.13.0 Up 1 minute (healthy)
registry goharbor/registry-photon:v2.13.0 Up 1 minute (healthy)
registryctl goharbor/harbor-registryctl:v2.13.0 Up 1 minute (healthy)
trivy-adapter goharbor/trivy-adapter-photon:v2.13.0 Up 1 minute (healthy)
Open https://registry.your_domain in your browser and log in as admin with the password from harbor.yml. Harbor's own container logs are also written to /var/log/harbor/ on the host.
Note
install.shregisters the containers withrestart: always, so Harbor starts again automatically when Docker starts after a reboot. Always rundocker composecommands for Harbor from/opt/harbor.
Step 5 - Renewing the certificate automatically
Harbor copies the certificate into its own configuration when you run the installer or the prepare script, and its Nginx container now occupies port 80. So renewals need two hooks: stop Harbor before Certbot's standalone server starts, and regenerate the configuration and start Harbor afterwards.
Create the pre-renewal hook:
sudo nano /etc/letsencrypt/renewal-hooks/pre/harbor-stop.sh
#!/usr/bin/env bash
set -euo pipefail
cd /opt/harbor
docker compose down
Create the post-renewal hook:
sudo nano /etc/letsencrypt/renewal-hooks/post/harbor-start.sh
#!/usr/bin/env bash
set -euo pipefail
cd /opt/harbor
./prepare --with-trivy
docker compose up -d
Make both scripts executable and test the renewal process:
sudo chmod +x /etc/letsencrypt/renewal-hooks/pre/harbor-stop.sh /etc/letsencrypt/renewal-hooks/post/harbor-start.sh
sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/registry.your_domain/fullchain.pem (success)
The dry run stops and starts Harbor, so expect about a minute of downtime. Certbot's systemd timer only runs the hooks when a certificate is actually due for renewal, roughly every 60 days.
Step 6 - Creating a project and pushing an image
In Harbor, images live in projects. Each project has its own members, visibility and security policies. In the UI, go to Projects > New Project, name it myapp, leave Public unchecked and select OK.
Open the project, go to Configuration and enable:
- Automatically scan images on push, so every new image is scanned by Trivy.
- Prevent vulnerable images from running, with severity Critical, if you want Harbor to refuse pulls of images with critical CVEs.
Select Save. Now, from any machine with Docker, log in to the registry:
docker login registry.your_domain
Login Succeeded
Tag an existing image with the registry hostname and project, then push it:
docker pull nginx:stable
docker tag nginx:stable registry.your_domain/myapp/nginx:stable
docker push registry.your_domain/myapp/nginx:stable
The push refers to repository [registry.your_domain/myapp/nginx]
...
stable: digest: sha256:... size: 1778
In the UI, open Projects > myapp > Repositories > myapp/nginx. The artifact shows a vulnerability summary after a few seconds. Select the artifact and open the Vulnerabilities tab to see each CVE, the affected package and the version that fixes it. You can also trigger a scan manually with the Scan button, for example after Trivy's database has been updated.
Step 7 - Adding users and robot accounts
For people, create users under Administration > Users (or connect LDAP or OIDC under Administration > Configuration > Authentication) and add them to projects under Projects > myapp > Members with a role:
| Role | Can do |
|---|---|
| Limited Guest | Pull images, without seeing the member list or logs. |
| Guest | Pull images and read project data. |
| Developer | Push and pull images. |
| Maintainer | Developer rights plus scanning, deleting images and managing policies. |
| Project Admin | Full control of the project, including members. |
For pipelines, use a robot account instead of a personal login. Robot accounts are scoped to a project, can expire and can be revoked without affecting any person. Go to Projects > myapp > Robot Accounts > New Robot Account, name it ci, set an expiration and grant the Push and Pull permissions on repositories. Harbor shows the secret once; copy it.
The robot's full name looks like robot$myapp+ci. Because it contains $, quote it with single quotes in the shell:
echo "your_robot_secret" | docker login registry.your_domain -u 'robot$myapp+ci' --password-stdin
In GitLab CI, store the name and secret as masked variables (HARBOR_USER, HARBOR_PASSWORD) and use them in a build job:
build-image:
image: docker:27
services:
- docker:27-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
IMAGE: registry.your_domain/myapp/web
script:
- echo "$HARBOR_PASSWORD" | docker login registry.your_domain -u "$HARBOR_USER" --password-stdin
- docker build -t "$IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker push "$IMAGE:$CI_COMMIT_SHORT_SHA"
Step 8 - Keeping storage under control
Deleting a tag in Harbor only removes the reference; the layers stay on disk until garbage collection runs. Two features handle this:
- Tag retention (Projects > myapp > Policy > Tag Retention): for example, keep the 10 most recently pushed artifacts per repository and delete the rest.
- Garbage collection (Administration > Clean Up): removes layers that no artifact references anymore. Schedule it weekly, at a time with little push activity.
To back up Harbor, stop it and archive the data directory together with the configuration:
cd /opt/harbor
sudo docker compose down
sudo tar -czf /var/backups/harbor-$(date +%F).tar.gz /data /opt/harbor/harbor.yml
sudo docker compose up -d
To upgrade Harbor, follow the upgrade notes of the target release; upgrades between minor versions include a database migration step.
Troubleshooting
docker login fails with x509: certificate signed by unknown authority or certificate is valid for ..., not registry.your_domain. The hostname in harbor.yml does not match the certificate or the name you are using. Fix harbor.yml, then run sudo docker compose down, sudo ./prepare --with-trivy and sudo docker compose up -d in /opt/harbor.
unauthorized: unauthorized to access repository when pushing. The user or robot account has no push permission on that project, or the project name in the image tag is wrong. The path must be registry.your_domain/<project>/<repository>:<tag>.
Pushes of large images fail with 413 or time out. Check that no other proxy in front of Harbor limits the request size. Harbor's own Nginx does not limit upload size.
Scans stay in "Pending" or fail. Check the Trivy adapter with sudo docker compose logs trivy-adapter. Trivy downloads its vulnerability database from the internet, so the server needs outbound HTTPS access.
Conclusion
Harbor is now running on Ubuntu 24.04 with a trusted certificate that renews automatically, Trivy scanning on every push, and a robot account your pipelines can use to publish images. Next, you can connect Harbor to your identity provider through OIDC, set up replication to a second Harbor instance for disaster recovery, or configure Harbor as a pull-through cache of Docker Hub with a proxy cache project.