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.