Secure On-Premise Harbor Deployment with Cloudflare Tunnel and NGINX (Proxmox + VM)
This guide details how to deploy a self-hosted Harbor container registry inside a private VM and expose it securely through Cloudflare Tunnel using NGINX running on a Proxmox host.
Architecture Overview
Browser / Docker Client
│ HTTPS (TLS)
▼
Cloudflare Edge Network
│ (secure tunnel)
▼
cloudflared (on Proxmox host)
│ forwards HTTPS → localhost:443
▼
NGINX (on Proxmox)
│ HTTPS termination (Origin CA)
▼
Harbor VM (192.168.1.182:8030)
HTTP backend
No public ports exposed — all traffic securely tunnels through Cloudflare.
1. Harbor VM Configuration
File: /opt/harbor/harbor/harbor.yml
hostname: 192.168.1.182
http:
port: 8030
external_url: https://harbor-registry.cloudmateria.com
Rebuild Harbor configuration:
cd /opt/harbor/harbor
sudo ./prepare
sudo docker-compose down -v
sudo docker-compose up -d
Test locally:
curl http://192.168.1.182:8030
Should return Harbor HTML.
2. NGINX Reverse Proxy (Proxmox Host)
File: /etc/nginx/sites-enabled/harbor-registry.cloudmateria.com
server {
listen 443 ssl;
server_name harbor-registry.cloudmateria.com;
ssl_certificate /etc/nginx/ssl/cloudmateria.pem;
ssl_certificate_key /etc/nginx/ssl/cloudmateria.key;
location / {
proxy_pass http://192.168.1.182:8030;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
Restart and validate:
sudo nginx -t
sudo systemctl reload nginx
Test locally:
curl -vk https://127.0.0.1 --resolve harbor-registry.cloudmateria.com:443:127.0.0.1
Should show Harbor HTML output.
3. Cloudflare Tunnel Configuration
File: /etc/cloudflared/config.yml
tunnel: harbor-tunnel
credentials-file: /root/.cloudflared/<UUID>.json
ingress:
- hostname: harbor-registry.cloudmateria.com
service: https://127.0.0.1:443
originRequest:
noTLSVerify: true
- service: http_status:404
Restart Cloudflared:
sudo systemctl restart cloudflared
Verify DNS route:
cloudflared tunnel route dns harbor-tunnel harbor-registry.cloudmateria.com
This creates a CNAME in Cloudflare → <UUID>.cfargotunnel.com.
4. Verification Commands
| Test | Command | Expected |
|---|---|---|
| Local Harbor | curl http://192.168.1.182:8030 | Harbor HTML |
| Local HTTPS | curl https://127.0.0.1 | Harbor HTML |
| Public HTTPS | curl https://harbor-registry.cloudmateria.com | Harbor HTML |
| Browser | https://harbor-registry.cloudmateria.com | Harbor login page |
5. Security Highlights
| Layer | Encryption | Notes |
|---|---|---|
| Browser → Cloudflare | ✅ Full HTTPS | TLS 1.3 Edge encryption |
| Cloudflare → Proxmox (cloudflared) | ✅ HTTPS Tunnel | Encrypted via Argo Tunnel |
| NGINX → Harbor | ⚙️ HTTP (internal only) | Can be upgraded to HTTPS via Origin CA |
6. Optional: Enable End-to-End TLS (Cloudflare → Harbor)
If you prefer full encryption:
- Generate a Cloudflare Origin CA certificate.
- Copy cert/key to Harbor VM (
/data/cert/). - Edit Harbor’s config:yaml
https: port: 443 certificate: /data/cert/origin.crt private_key: /data/cert/origin.key - Update Cloudflared config:yaml
service: https://192.168.1.182:443 originRequest: noTLSVerify: true
Rebuild Harbor and restart services.
7. Final Results
- Secure public access at
https://harbor-registry.cloudmateria.com - No exposed ports
- Harbor fully functional behind Cloudflare
- Supports Docker login, push, and K8s image pulls
Summary Table
| Component | Role | Address | Protocol |
|---|---|---|---|
| Harbor VM | Registry service | 192.168.1.182:8030 | HTTP |
| NGINX | Reverse proxy | 127.0.0.1:443 | HTTPS |
| Cloudflared | Secure tunnel to Cloudflare | localhost:443 | HTTPS |
| Public Domain | External access | harbor-registry.cloudmateria.com | HTTPS |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway | Cloudflared → NGINX protocol mismatch | Set service: https://127.0.0.1:443 |
| 400 Bad Request | Sent HTTP to HTTPS port | Same fix as above |
./login redirect | Missing external_url in harbor.yml | Add full domain and rebuild Harbor |
| Wrong app loads | Another NGINX default server takes precedence | Use listen 127.0.0.1:443 ssl http2; |
End Result: You now have a private Harbor registry, securely accessible worldwide through Cloudflare, with no direct exposure to the internet — perfect for use with Kubernetes, CI/CD pipelines, or private image distribution.