Real-World Problem: The Breaking Point of Free Registry Limits and Unverified Containers
A few months back, my HomeLab builds ground to an abrupt halt. I had three local GitLab runners building microservices, a four-node k3s cluster pulling test images, and several cron jobs running maintenance tasks. Every pipeline suddenly blew up with 429 Too Many Requests. Docker Hub had clamped down on my external IP with its strict 100-pulls-per-6-hours limit for anonymous users.
Beyond the pull caps, a more dangerous issue quietly slipped into my stack: blind trust. When testing base images or pulling experimental containers, I had zero automated vulnerability checks. A base image with critical remote code execution flaws could sit on my nodes for months undetected. Worse, my worker nodes pulled whatever binary matched the tag, with no cryptographic proof verifying that the image came from my build pipeline.
Root Cause Analysis: Why Docker Hub and Minimal Registries Fall Short
Most HomeLab engineers start with one of two basic setups. They either depend on public SaaS registries or spin up the standard, bare-bones registry:2 container.
Both models collapse quickly once your lab grows beyond a couple of machines:
- Bandwidth Waste and Upstream Latency: Pushing a 1.2GB microservice container to an external registry over a 50Mbps residential upload is painful. Pulling that exact image back down to a server on the same local gigabit switch makes no sense.
- No Automated Security Checks: The stock
registry:2container is essentially an HTTP API over a disk folder. It has no vulnerability engine, no CVE database, and no mechanism to halt a deployment if an image is compromised. - Missing Provenance and Policy Controls: Minimal registries lack granular Role-Based Access Control (RBAC), tag immutability, and digital signature checks. Anyone on the local network can overwrite your
latesttag with untested or broken code.
Solutions Compared: Finding the Right HomeLab Registry
When picking a dedicated platform for my lab, I evaluated three main contenders:
| Solution | Resource Footprint | Security & Vulnerability Scanning | Image Signing Support | Best Suited For |
|---|---|---|---|---|
| Docker Registry v2 | Minimal (~50-100MB RAM) | None (Requires external CLI tooling) | None | Temporary local staging without security requirements. |
| Nexus OSS / JFrog Artifactory | Heavy (>4GB RAM, JVM-based) | Paid tiers only (Xray / Pro features) | Complex plugin configuration | Enterprise multi-artifact setups (Maven, npm, PyPI + Docker). |
| Project Harbor | Moderate (~2GB to 3GB RAM) | Built-in Trivy scanner integration | Native Cosign and Notary support | HomeLabs, GitOps pipelines, and container governance. |
Docker Registry v2 is lightweight, but it ignores container security completely. Nexus and Artifactory demand heavy system resources and reserve automated vulnerability scanners for expensive commercial licenses. Harbor strikes the ideal balance. As a CNCF-graduated open-source project, it provides native Trivy scanning, tag immutability, robot accounts, and image verification without enterprise paywalls.
Best Approach: Deploying Harbor on Docker with Trivy and Cosign
Below is the battle-tested configuration I use to run Harbor on an Ubuntu 24.04 LTS host using Docker Compose, complete with Trivy security scans and Cosign signing.
Step 1: Host Prerequisites
Set aside a virtual machine or bare-metal host with at least 2 vCPUs, 4GB of RAM, and 50GB of free storage. Verify your Docker Engine and Compose plugin installations:
# Verify Docker and Compose installations
docker --version
docker compose version
# Create the working directory for Harbor
sudo mkdir -p /opt/harbor
cd /opt/harbor
Step 2: Generate Certificates and Configure the Local Trust Store
Modern container engines reject insecure HTTP registries by default. If you run an internal CA like Step-CA or pfSense, issue a certificate for core.harbor.local. Otherwise, generate a dedicated local Certificate Authority and server certificate using OpenSSL:
# Generate root CA private key and certificate (valid for 10 years)
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -sha512 -days 3650 \
-subj "/C=US/ST=Lab/L=Home/O=HomeLab/CN=HomeLab-RootCA" \
-key ca.key -out ca.crt
# Generate server key and signing request (CSR)
openssl genrsa -out harbor.local.key 4096
openssl req -sha512 -new \
-subj "/C=US/ST=Lab/L=Home/O=HomeLab/CN=core.harbor.local" \
-key harbor.local.key -out harbor.local.csr
# Define Subject Alternative Names (SAN)
cat > v3.ext <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = core.harbor.local
DNS.2 = harbor.local
EOF
# Sign the server certificate with your local CA
openssl x509 -req -sha512 -days 365 \
-extfile v3.ext \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-in harbor.local.csr -out harbor.local.crt
# Store certificates in Harbor's certificate directory
sudo mkdir -p /data/cert
sudo cp harbor.local.crt /data/cert/
sudo cp harbor.local.key /data/cert/
Step 3: Download and Configure Harbor
Download the latest offline installer bundle from GitHub to avoid network delays during deployment:
cd /opt/harbor
wget https://github.com/goharbor/harbor/releases/download/v2.10.0/harbor-offline-installer-v2.10.0.tgz
tar xvf harbor-offline-installer-v2.10.0.tgz
cp harbor.yml.tmpl harbor.yml
Open harbor.yml and adjust your hostname, certificate locations, and administrative credentials:
# harbor.yml core configuration
hostname: core.harbor.local
# Redirect plain HTTP traffic or keep it for internal proxies
http:
port: 80
https:
port: 443
certificate: /data/cert/harbor.local.crt
private_key: /data/cert/harbor.local.key
# Set a strong administrative password
harbor_admin_password: ChangeThisStrongSecret2026!
# Directory for data persistence (images, database, chart repo)
data_volume: /data/harbor
Step 4: Run the Installer with the Trivy Scanner
Launch Harbor with Trivy enabled by passing the --with-trivy flag. The installer generates the docker-compose.yml file and spins up nine coordinated services:
sudo ./install.sh --with-trivy
# Confirm all containers are healthy
docker compose ps
Step 5: Trust the Root CA on Docker Clients and Build Nodes
Tools like Docker and Cosign will reject connections until they trust your local root CA. Run these commands on any workstation, CI runner, or client server that talks to Harbor:
# 1. Configure the Docker daemon trust path
sudo mkdir -p /etc/docker/certs.d/core.harbor.local
sudo cp /opt/harbor/ca.crt /etc/docker/certs.d/core.harbor.local/ca.crt
# 2. Add CA to OS root store so Cosign and curl trust it
sudo cp /opt/harbor/ca.crt /usr/local/share/ca-certificates/homelab-ca.crt
sudo update-ca-certificates
# 3. Restart the Docker daemon to apply changes
sudo systemctl restart docker
Also verify that your client resolves core.harbor.local to your Harbor host IP, either via internal DNS (such as Pi-hole or AdGuard Home) or through an entry in /etc/hosts.
Step 6: Automate Vulnerability Policies in the Web UI
Open your browser and navigate to https://core.harbor.local. Sign in with the admin account and the password configured in your YAML file.
- Under Administration > Interrogation Services, verify that Trivy is active. Set a cron schedule (like
0 0 * * *) to refresh the vulnerability database nightly. - Select Projects, click New Project, name it
production, and verify it is set to private. - Open the project settings tab. Check Automatically scan images on push.
- Toggle Prevent vulnerable images from running and choose High or Critical. Harbor will now actively block downstream pulls for any image containing unpatched high-severity CVEs.
Step 7: Sign Container Images with Cosign
Scanning catches known vulnerabilities. Cryptographic signing guarantees provenance and blocks tampering. Install Cosign on your workstation or CI runner to sign images directly against Harbor:
# Generate a signing keypair (choose a secure password for cosign.key)
cosign generate-key-pair
# Authenticate Docker against your Harbor registry
docker login core.harbor.local -u admin
# Build, tag, and push your sample container
docker tag alpine:latest core.harbor.local/production/my-app:1.0.0
docker push core.harbor.local/production/my-app:1.0.0
# Sign the image digest directly in Harbor
cosign sign --key cosign.key core.harbor.local/production/my-app:1.0.0
# Verify the image signature
cosign verify --key cosign.pub core.harbor.local/production/my-app:1.0.0
Check the Harbor UI under your production project. Next to your tag, you will see two badges: a detailed Trivy CVE breakdown and a green checkmark confirming a verified Cosign signature.
Running your own Harbor registry frees your lab from external rate limits. It cuts pull times to seconds across your local network. Best of all, it gives your HomeLab the same scanning and signing guardrails used in enterprise clusters.

