← All articles
CONTAINERS Self-Hosted Container Registry with Harbor 2026-02-14 · 7 min read · harbor · container-registry · docker

Self-Hosted Container Registry with Harbor

Containers 2026-02-14 · 7 min read harbor container-registry docker kubernetes images ci-cd

If you build custom Docker images in your homelab, they have to live somewhere. Docker Hub works, but your private images count against a storage limit, pulls are rate-limited, and your homelab's custom images are now sitting on someone else's infrastructure. Running docker push to a registry on the other side of the internet, only to docker pull it back on the same network, is slow and unnecessary.

Harbor is a CNCF-graduated container registry designed for enterprise use but perfectly suited to homelabs. Beyond basic image storage, it provides vulnerability scanning (via Trivy), role-based access control, image replication between registries, content signing, and a clean web UI for managing everything. It's what you'd build if you took a basic Docker registry and added everything you actually need.

Harbor container registry logo

Why Not Just Use Docker Registry?

Docker's official registry:2 image gives you a functional registry in one container. But "functional" is doing a lot of heavy lifting there. Here's what you're missing:

Feature Docker Registry Harbor
Web UI None (API only) Full management interface
Vulnerability scanning No Built-in Trivy integration
RBAC No (basic htpasswd auth) Projects, roles, robot accounts
Image replication No Push/pull between registries
Garbage collection Manual, requires downtime Scheduled, online
Audit logging Minimal Full operation logs
Helm chart hosting No Native support
OCI artifact support Basic Full (Helm charts, WASM, SBOMs)
Content trust Notary (separate setup) Integrated Cosign/Notation

For a single developer pushing a few images, the basic registry is fine. But once you have multiple projects, want to know if your images contain known vulnerabilities, or need to share access with others, Harbor is worth the slightly larger footprint.

Installation

Harbor provides an offline installer that bundles everything into Docker Compose. This is the recommended approach for homelabs.

Prerequisites

Download and Configure

# Download the latest offline installer
HARBOR_VERSION="2.11.0"
wget "https://github.com/goharbor/harbor/releases/download/v${HARBOR_VERSION}/harbor-offline-installer-v${HARBOR_VERSION}.tgz"
tar xzf "harbor-offline-installer-v${HARBOR_VERSION}.tgz"
cd harbor

# Copy the template configuration
cp harbor.yml.tmpl harbor.yml

Edit harbor.yml with your settings:

# harbor.yml
hostname: registry.yourdomain.com

# HTTPS configuration
https:
  port: 443
  certificate: /etc/letsencrypt/live/registry.yourdomain.com/fullchain.pem
  private_key: /etc/letsencrypt/live/registry.yourdomain.com/privkey.pem

# Change the admin password immediately after first login
harbor_admin_password: Harbor12345

# Database configuration (Harbor's internal PostgreSQL)
database:
  password: root123
  max_idle_conns: 50
  max_open_conns: 1000

# Storage backend
data_volume: /data/harbor

# Trivy vulnerability scanner
trivy:
  insecure: false
  github_token: ""  # Optional: avoids rate limiting for vulnerability DB updates
  skip_update: false
  offline_scan: false

# Log settings
log:
  level: info
  local:
    rotate_count: 50
    rotate_size: 200M
    location: /var/log/harbor

# Redis (used internally by Harbor)
# External Redis is supported but not needed for homelab scale

Run the Installer

# Prepare configuration and start all services
sudo ./install.sh --with-trivy

# The installer will:
# 1. Generate docker-compose.yml from your harbor.yml
# 2. Pull and load all container images from the offline bundle
# 3. Create the Docker network and volumes
# 4. Start all services

After installation, Harbor runs approximately 9 containers:

docker compose ps
# NAME                    STATUS
# harbor-core             running
# harbor-db               running
# harbor-jobservice       running
# harbor-log              running
# harbor-portal           running
# nginx                   running
# redis                   running
# registry                running
# registryctl             running
# trivy-adapter           running

First Login

Navigate to https://registry.yourdomain.com and log in with admin / Harbor12345 (or whatever you set). Change the admin password immediately.

Configuring Docker Clients

Every machine that needs to push or pull from your registry needs to trust it.

If Using a Trusted Certificate

If your certificate is from Let's Encrypt or a CA your machines already trust, Docker works immediately:

docker login registry.yourdomain.com
# Enter username and password

If Using a Self-Signed or Private CA Certificate

# Create the Docker certificate directory
sudo mkdir -p /etc/docker/certs.d/registry.yourdomain.com/

# Copy your CA certificate
sudo cp ca.crt /etc/docker/certs.d/registry.yourdomain.com/

# Restart Docker
sudo systemctl restart docker

# Now login
docker login registry.yourdomain.com

Projects and RBAC

Harbor organizes images into projects. Each project has its own access control, vulnerability policies, and storage quota.

Creating a Project

# Via the API (or use the web UI)
curl -u admin:YourPassword -X POST \
  "https://registry.yourdomain.com/api/v2.0/projects" \
  -H "Content-Type: application/json" \
  -d '{
    "project_name": "homelab",
    "public": false,
    "storage_limit": -1,
    "metadata": {
      "auto_scan": "true",
      "severity": "high"
    }
  }'

The auto_scan: true metadata means every pushed image gets scanned automatically. The severity threshold determines when images are blocked from being pulled.

Role-Based Access

Harbor supports these roles per project:

Role Permissions
Project Admin Full control: manage members, labels, configurations, scanners
Maintainer Push/pull images, scan, create tags, create robot accounts
Developer Push/pull images
Guest Pull images only
Limited Guest Pull images only (no Helm charts or view scan results)

For a homelab, create a Developer account for CI/CD systems and a Guest account for Kubernetes pull access.

Robot Accounts

Robot accounts are service accounts designed for automation. They don't have a password that can log into the web UI.

# Create a robot account for CI/CD
curl -u admin:YourPassword -X POST \
  "https://registry.yourdomain.com/api/v2.0/projects/homelab/robots" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-pipeline",
    "duration": -1,
    "description": "CI/CD push access",
    "disable": false,
    "level": "project",
    "permissions": [
      {
        "kind": "project",
        "namespace": "homelab",
        "access": [
          {"resource": "repository", "action": "push"},
          {"resource": "repository", "action": "pull"},
          {"resource": "tag", "action": "create"}
        ]
      }
    ]
  }'

The response includes a secret -- this is the robot account's token. Store it securely (in Vault, perhaps).

Vulnerability Scanning with Trivy

Trivy is integrated into Harbor as a pluggable scanner. Every image you push can be automatically scanned against the CVE database.

Manual Scan

From the Harbor UI, navigate to any repository and click "Scan." Or via API:

# Trigger a scan on a specific artifact
curl -u admin:YourPassword -X POST \
  "https://registry.yourdomain.com/api/v2.0/projects/homelab/repositories/my-app/artifacts/latest/scan"

Scan Results

Trivy categorizes vulnerabilities by severity:

Enforce Vulnerability Policies

In project settings, you can block pulls of images that exceed a severity threshold:

  1. Navigate to your project in the Harbor UI
  2. Go to Configuration
  3. Set Prevent vulnerable images from running to enabled
  4. Choose the severity threshold (e.g., Critical)

Now, any docker pull for an image with critical vulnerabilities returns an error. This is genuinely useful -- it prevents you from deploying a container image with a known critical CVE, even in a homelab.

Keeping the Vulnerability Database Updated

Trivy downloads its vulnerability database on first scan and periodically updates it. If your Harbor instance doesn't have direct internet access, you can pre-download the database:

# Download DB on a machine with internet access
trivy image --download-db-only

# The DB is cached at ~/.cache/trivy/
# Copy it to your Harbor's Trivy adapter volume

Image Replication

Harbor can replicate images between registries. This is useful for:

Setting Up a Pull Replication Rule

This example caches popular images from Docker Hub:

  1. Go to Registries in the Harbor admin panel
  2. Add Docker Hub as a registry endpoint:
    • Provider: Docker Hub
    • Endpoint URL: https://hub.docker.com
    • (Optional) Access credentials for higher rate limits
  3. Go to Replications and create a new rule:
    • Name: cache-docker-hub
    • Replication mode: Pull-based
    • Source: Docker Hub registry
    • Source filter: library/nginx, library/postgres, grafana/grafana (or use ** for everything)
    • Trigger: Scheduled (daily)
    • Destination: Your project (e.g., docker-hub-cache)

Now your Kubernetes nodes or Docker hosts can pull from registry.yourdomain.com/docker-hub-cache/library/nginx:latest instead of hitting Docker Hub.

CI/CD Integration

Gitea Actions / GitHub Actions

# .gitea/workflows/build.yml
name: Build and Push
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Login to Harbor
        uses: docker/login-action@v3
        with:
          registry: registry.yourdomain.com
          username: robot$homelab+ci-pipeline
          password: ${{ secrets.HARBOR_ROBOT_TOKEN }}

      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            registry.yourdomain.com/homelab/my-app:latest
            registry.yourdomain.com/homelab/my-app:${{ github.sha }}

Kubernetes Integration

Create an image pull secret for your cluster:

kubectl create secret docker-registry harbor-creds \
  --docker-server=registry.yourdomain.com \
  --docker-username='robot$homelab+k8s-pull' \
  --docker-password='robot-token-here' \
  -n default

Reference it in your deployments:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      imagePullSecrets:
        - name: harbor-creds
      containers:
        - name: my-app
          image: registry.yourdomain.com/homelab/my-app:latest

Garbage Collection

Deleted images don't free disk space until garbage collection runs. Harbor supports scheduled garbage collection that runs without downtime:

  1. Go to Administration > Clean Up
  2. Configure the schedule (weekly is usually fine for homelabs)
  3. Optionally delete untagged artifacts automatically

Or trigger it via API:

curl -u admin:YourPassword -X POST \
  "https://registry.yourdomain.com/api/v2.0/system/gc/schedule" \
  -H "Content-Type: application/json" \
  -d '{
    "schedule": {
      "type": "Weekly",
      "cron": "0 0 0 * * 0"
    },
    "parameters": {
      "delete_untagged": true,
      "dry_run": false
    }
  }'

Resource Usage

Harbor's footprint is larger than a bare Docker registry, but reasonable for what you get:

Component RAM CPU Disk
Core ~300 MB Low Minimal
Portal (UI) ~100 MB Low Minimal
Database (PostgreSQL) ~200 MB Low Grows with metadata
Redis ~50 MB Low Minimal
Registry ~100 MB Low Image storage
Trivy adapter ~500 MB Spikes during scans ~1 GB for vuln DB
Job service ~100 MB Low Minimal
Total ~1.5 GB idle 2 cores 2 GB + images

The Trivy vulnerability database is the largest fixed cost. During scans, CPU usage spikes but settles quickly.

Maintenance Tips

Harbor turns "where do I put my Docker images" from an afterthought into a properly managed part of your homelab infrastructure. The vulnerability scanning alone makes it worthwhile -- knowing which of your running containers have known CVEs is the kind of visibility that costs real money in production environments but is free when you self-host.