Analytics cookies only (Google Analytics, PostHog). No ad networks, no cross-site tracking. See our privacy policy.
Self-host Harbor
updated Oct 2026deployed by usprices checked · Jul 2026Install verified on Ubuntu 26.04 · Oct 2026 · how we test
We earn commissions when you shop through the links below. Full disclosure →
A full container registry for teams: projects with role-based access, a web UI, image and Helm chart storage, replication to and from other registries, retention and immutability rules, audit logs, robot accounts and optional Trivy vulnerability scanning, all on top of the CNCF Distribution registry. Apache-2.0 licensed and a CNCF graduated project, written in Go with an Angular portal; it runs as a set of containers (core, jobservice, portal, registry, PostgreSQL, Redis, nginx) generated and started by its own installer.
Runs on a VPS from $23/mo (Kamatera 8GB, 8 GB RAM) · prices checked Sep 2026
Our recommendation
Pick Harbor when a bare registry is not enough and you want what a team needs around image storage: projects with role-based access, a web UI, robot accounts for CI, replication to and from other registries, tag retention and immutability rules, audit logs and optional Trivy vulnerability scanning, with Helm charts stored as OCI artifacts alongside images. It is Apache-2.0, a CNCF graduated project, Go services with an Angular portal, and it runs the same Distribution registry as a component underneath. Our snippet follows the install docs: download the online installer, copy `harbor.yml.tmpl` to `harbor.yml`, set the hostname and generated admin and database passwords, drop the `https` block so the installer configures plain HTTP on port 80, and run `install.sh`, which pulls the images and starts the generated compose project. Upstream's prerequisites put the floor at 2 CPUs, 4 GB of RAM and 40 GB of disk (8 GB and 160 GB recommended), which is our documented floor. The catalog's Distribution is the engine alone: one container, htpasswd auth, no UI. Harbor is what you run when more than one person or pipeline pushes.
What you need
Any VPS with at least 4096 MB of RAM
A domain you control — most self-hosted setups need HTTPS in front of them
About an afternoon — budget time for troubleshooting
Paid link — we earn a commission if you shop through it.
Install
Run these commands on your server:
# Harbor — container registry with a web UI, projects, RBAC, replication and optional Trivy scanning — based on the Harbor install docs (online installer + harbor.yml), pinned to v2.15.4echo "HARBOR_ADMIN_PASSWORD=$(openssl rand -hex 16)" > .env. ./.envcurl -fsSLO https://github.com/goharbor/harbor/releases/download/v2.15.4/harbor-online-installer-v2.15.4.tgztar xzf harbor-online-installer-v2.15.4.tgzcd harborcp harbor.yml.tmpl harbor.yml# hostname is the address clients will use for the UI and docker login; upstream says never localhost or 127.0.0.1sed -i "s/^hostname: .*/hostname: SERVER_IP/" harbor.yml# HTTP only: the installer's prepare step configures nginx for plain HTTP on http.port (80) when the https block is absentsed -i '/^https:/,/^ private_key:/d' harbor.ymlsed -i "s/^harbor_admin_password: .*/harbor_admin_password: $HARBOR_ADMIN_PASSWORD/" harbor.ymlsed -i "s/^ password: root123$/ password: $(openssl rand -hex 16)/" harbor.yml# pulls the Harbor images, writes /data and /var/log/harbor, generates docker-compose.yml and starts itsudo ./install.shtimeout 300 bash -c 'until curl -fsS http://localhost:80/api/v2.0/ping >/dev/null; do sleep 5; done'# → http://SERVER_IP:80 — sign in as admin with HARBOR_ADMIN_PASSWORD from ~/.env; docker login SERVER_IP from another machine needs the host in that daemon's insecure-registries until you add TLS
Harbor's harbor.yml reference, prerequisites and installer docs flag these before you point a daemon at it:
non-negotiableThe template ships admin/Harbor12345 and the database password root123; our snippet generates both. harbor_admin_password is only read the first time Harbor starts, so later changes go through the UI, and the docs say not to use localhost or 127.0.0.1 as hostname because clients must reach Harbor by that name (the prepare step refuses 127.0.0.1 and the template placeholder outright). We write the server address in; replace it with the DNS name people will use before anyone pushes, since it ends up in the URLs Harbor hands out.
non-negotiableThe installer writes state to /data (data_volume: database, Redis, registry blobs and job logs) and logs to /var/log/harbor; /data outlives the containers, so a second install.sh with a fresh harbor.yml meets a database that still holds the old password and the core never comes up (our verification run hit exactly that until /data was wiped). It also generates docker-compose.yml plus a common/config tree from harbor.yml inside the extracted harbor/ directory by running the goharbor/prepare image. Keep that directory: docker compose commands and any re-run of prepare after editing harbor.yml happen there, and it ran as root, so expect to prefix them with sudo. Back up /data and harbor.yml together.
non-negotiableWe removed the https block, so the prepare step configures nginx for HTTP only. Upstream allows this only for air-gapped test or development environments and says HTTP elsewhere exposes you to man-in-the-middle attacks; it also means every Docker daemon that logs in needs the host in insecure-registries in daemon.json, followed by systemctl restart docker and a docker compose down -v / up -d of Harbor. Trivy scanning is not installed unless you run install.sh --with-trivy.
non-negotiableHarbor's own nginx listens on http.port (80), and on https.port (443) once you restore the https block with certificate and private_key paths and re-run the installer. If you put another reverse proxy in front, set external_url to the public address, after which hostname is ignored. The installer needs Docker Engine with either the compose plugin or docker-compose; the online variant pulls every image from Docker Hub, which the docs warn can take a while.