Running Your Own Private Image Registry (Registry / Harbor)
Spin up a private registry on your own server with the official image, secure it with TLS and a login, and learn when Harbor is worth it.
Once you start building your own container images, you usually don't want to push them to a public registry. A private registry keeps your images on a server you control, speeds up pulls inside your network, and slots neatly into CI/CD and Kubernetes.
Why run a private registry
- Privacy: internal images stay off the public internet, so source and config don't leak.
- Speed and reliability: pulling from your own data center beats the public internet and dodges rate limits.
- Control and retention: you own the tags, history, and scan results.
Spin one up with the official image
The official registry:2 image is a lightweight registry you can start in a few commands. The steps below assume an Ubuntu/Debian server with Docker already installed.
Create directories for certificates, credentials, and data:
sudo mkdir -p /opt/registry/{certs,auth,data}
Set up TLS
Always use TLS in production. If you have a domain, Let's Encrypt is the easy path; for testing you can self-sign:
sudo openssl req -x509 -newkey rsa:4096 -days 365 -nodes \
-keyout /opt/registry/certs/domain.key \
-out /opt/registry/certs/domain.crt \
-subj "/CN=registry.example.com" \
-addext "subjectAltName=DNS:registry.example.com"
Set up a login
Generate a bcrypt credential with htpasswd:
sudo apt-get install -y apache2-utils
htpasswd -Bbn admin 'YourStrongPassword' | sudo tee /opt/registry/auth/htpasswd
Start the registry
docker run -d --restart=always --name registry -p 5000:5000 \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/certs:/certs \
-v /opt/registry/auth:/auth \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
-e REGISTRY_AUTH=htpasswd \
-e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
registry:2
Push and pull images
Log in, retag the image with the registry prefix using docker tag, then push:
docker login registry.example.com:5000
docker tag myapp:1.0 registry.example.com:5000/myapp:1.0
docker push registry.example.com:5000/myapp:1.0
On another machine, pull it back:
docker login registry.example.com:5000
docker pull registry.example.com:5000/myapp:1.0
Trusting the certificate on clients
With a self-signed certificate, clients fail with x509: certificate signed by unknown authority. Drop the cert into Docker's trust directory:
sudo mkdir -p /etc/docker/certs.d/registry.example.com:5000
sudo cp domain.crt /etc/docker/certs.d/registry.example.com:5000/ca.crt
The directory name must match the registry host and port exactly. If you use a cert from a trusted CA like Let's Encrypt, you can skip this entirely. Don't reach for insecure-registries to silence the error — that throws away encryption.
Stepping up to Harbor
As your team grows, a bare registry starts to feel thin. Harbor, a graduated CNCF project, layers extra features on top:
- A web UI with per-project RBAC, so you can isolate images by project and user.
- Vulnerability scanning (via Trivy) that flags CVEs on push.
- Image signing, replication, garbage collection, and audit logs.
Harbor ships online and offline installers: edit harbor.yml with your hostname and certificate, run ./install.sh, and it stands itself up on Docker Compose under the hood.
Summary
A private registry keeps your images firmly in your own hands. With registry:2 you can stand up a minimal TLS-secured, password-protected registry in minutes, and docker tag/push/pull covers day-to-day use — just remember to have clients trust a self-signed cert. When the team outgrows that, move to Harbor for a UI, access control, and scanning.