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 sudo privileges.
  • A domain name such as registry.your_domain with 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:

  • hostname must be the exact name clients use to reach the registry. It is embedded in image references and tokens.
  • With https enabled, requests to port 80 are redirected to HTTPS.
  • harbor_admin_password is the initial password for the admin user. Replace the template's default Harbor12345 with a strong password. You can change it later in the UI.
  • database.password is the password of Harbor's internal PostgreSQL database. Change the default here too.
  • data_volume is 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.

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:

RoleCan do
Limited GuestPull images, without seeing the member list or logs.
GuestPull images and read project data.
DeveloperPush and pull images.
MaintainerDeveloper rights plus scanning, deleting images and managing policies.
Project AdminFull 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.