--- title: "How to Install Harbor Container Registry on Ubuntu 24.04" description: "Install Harbor on Ubuntu 24.04 with a Let's Encrypt certificate and Trivy scanning, then push images, scan them for CVEs and use robot accounts in CI pipelines." category: "CI/CD DevOps" language: en source: https://cubepath.com/docs/ci-cd-devops/harbor-container-registry-installation updated: 2026-09-25 --- # How to Install Harbor Container Registry on Ubuntu 24.04 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: ```bash sudo apt update sudo apt install docker.io docker-compose-v2 sudo systemctl enable --now docker ``` Verify the installation: ```bash sudo docker --version sudo docker compose version ``` ```output 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: ```bash sudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable ``` Install Certbot and request the certificate: ```bash sudo apt install certbot sudo certbot certonly --standalone -d registry.your_domain ``` ```output 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](https://github.com/goharbor/harbor/releases) and set it in a variable. The version below is an example; use the current one: ```bash 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`: ```bash sudo tar -xzf "harbor-online-installer-${HARBOR_VERSION}.tgz" -C /opt cd /opt/harbor ``` Create the configuration file from the template and open it: ```bash sudo cp harbor.yml.tmpl harbor.yml sudo nano harbor.yml ``` Edit the following keys. Leave the rest of the file as it is: ```yaml 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: ```bash 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: ```bash sudo ./install.sh --with-trivy ``` The last lines confirm a successful start: ```output [Step 5]: starting Harbor ... ... ----Harbor has been installed and started successfully.---- ``` Check that all containers are running and healthy: ```bash sudo docker compose ps ``` ```output 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.sh` registers the containers with `restart: always`, so Harbor starts again automatically when Docker starts after a reboot. Always run `docker compose` commands 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: ```bash sudo nano /etc/letsencrypt/renewal-hooks/pre/harbor-stop.sh ``` ```bash #!/usr/bin/env bash set -euo pipefail cd /opt/harbor docker compose down ``` Create the post-renewal hook: ```bash sudo nano /etc/letsencrypt/renewal-hooks/post/harbor-start.sh ``` ```bash #!/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: ```bash sudo chmod +x /etc/letsencrypt/renewal-hooks/pre/harbor-stop.sh /etc/letsencrypt/renewal-hooks/post/harbor-start.sh sudo certbot renew --dry-run ``` ```output 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: ```bash docker login registry.your_domain ``` ```output Login Succeeded ``` Tag an existing image with the registry hostname and project, then push it: ```bash docker pull nginx:stable docker tag nginx:stable registry.your_domain/myapp/nginx:stable docker push registry.your_domain/myapp/nginx:stable ``` ```output 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: ```bash 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: ```yaml 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: ```bash 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//:`. **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.