Docker 61 🐳 Setting Up Private Container Registries: Harbor, Nexus, and Docker Registry Server
A private container registry is the backbone of any serious container workflow. Public registries like Docker Hub are convenient for open-source images, but they impose pull rate limits, expose your image names to the world, and create a dependency on an external service that can go down or change its terms. For teams building proprietary software, the registry is where your build artifacts live—it needs to be fast, secure, and under your control.
The three main options for self-hosted registries sit on a spectrum from minimal to enterprise-grade. The official Docker Registry server is the simplest: a single container that speaks the Docker Registry HTTP API v2 protocol, stores images on the local filesystem, and does nothing else . Harbor, originally open-sourced by VMware, adds role-based access control, vulnerability scanning with Trivy, image replication between registries, and a full web UI . Nexus Repository Manager, from Sonatype, is a universal artifact repository that can host Docker images alongside Maven, npm, PyPI, and Helm charts, making it a single destination for all your build artifacts . This chapter walks through deploying all three, from the one-command Docker Registry to the Kubernetes-deployed Harbor and the multi-format Nexus.
Key point: The Docker Registry server is the minimal foundation; Harbor adds enterprise security and management; Nexus adds multi-format artifact storage. The right choice depends on whether you need a simple image store, a secured registry with scanning, or a universal artifact repository.
Why private registries exist
The pull rate limit problem. Docker Hub imposes rate limits on anonymous and free-tier pulls. In a CI/CD pipeline where dozens of builds run per day, each pulling base images, these limits become a bottleneck . A private registry has no such limits—you pull as often as your infrastructure allows.
The proprietary code problem. Your application images contain your code, your configurations, and sometimes your secrets. Pushing them to a public registry means they leave your infrastructure. A private registry keeps them internal . This is not just about secrecy; it is about compliance with data residency and regulatory requirements.
The speed problem. Pulling a 1 GB image from Docker Hub over a shared internet connection takes minutes. Pulling it from a registry on your local network takes seconds. When you are iterating on a Kubernetes deployment with multiple replicas, this difference compounds .
The control problem. With a public registry, you have no say in retention policies, no ability to scan images for vulnerabilities before deployment, and no audit trail of who pushed what. A private registry gives you all three .
The air-gap problem. Some environments have no internet access at all—classified networks, isolated industrial systems, or development environments behind strict firewalls. For these, a private registry is not a convenience but a necessity. You populate it with images from a connected machine, then serve them internally .
a. The Docker Registry server: minimal and direct
The official Docker Registry server is the simplest possible private registry. It is a single container that implements the Docker Registry HTTP API v2. It stores images on the filesystem, has no built-in authentication, and no web UI .
Running it is one command:
docker run -d -p 5000:5000 --restart always --name registry registry:2
This starts the registry on port 5000, stores data in the container’s filesystem (which is ephemeral unless you mount a volume), and restarts automatically if the container stops . The -p 5000:5000 maps the container port to the host port; if 5000 is already in use, change the host side, for example -p 5001:5000 .
For any real use, you must mount a volume so images survive container restarts:
docker run -d \
-p 5000:5000 \
--restart always \
--name registry \
-v /opt/registry/data:/var/lib/registry \
registry:2
The -v flag mounts the host directory /opt/registry/data to the container’s storage path. All pushed images are stored as files under this directory .
By default, the registry speaks plain HTTP. Docker clients refuse to connect to HTTP registries unless you explicitly mark them as insecure. You can do this by adding the registry’s address to /etc/docker/daemon.json:
{
"insecure-registries": ["192.168.1.100:5000"]
}
Then restart Docker . This is acceptable for a local development registry but not for production. For production, you wrap the registry behind an Nginx reverse proxy that terminates TLS .
Pushing an image to the registry requires retagging it with the registry address as the prefix:
docker tag my-app:latest localhost:5000/my-app:latest
docker push localhost:5000/my-app:latest
Pulling works the same way:
docker pull localhost:5000/my-app:latest
The localhost:5000 prefix tells Docker which registry to use. Images without a registry prefix default to Docker Hub .
The Docker Registry server has no built-in authentication. Anyone who can reach the port can push or pull. For any environment where the registry is exposed beyond localhost, you need to add authentication. The common approach is to run the registry with htpasswd authentication:
docker run -d \
-p 5000:5000 \
--restart always \
--name registry \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/auth:/auth \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
registry:2
You create the htpasswd file using the htpasswd utility from the apache2-utils package . Once auth is enabled, all push and pull operations require docker login.
b. Harbor: enterprise-grade registry
Harbor is the most feature-complete open-source registry. It wraps the Docker Distribution project (the same code as the Docker Registry server) with a management layer that adds access control, vulnerability scanning, image replication, and a web UI .
The core components are: Harbor Core (the API and authentication layer), Harbor Portal (the web UI), Harbor Registry (the actual image storage, based on Docker Distribution), Harbor Job Service (async tasks like replication and garbage collection), PostgreSQL (metadata storage), Redis (caching and message broker), and Trivy (vulnerability scanner) . All of these run as separate containers, orchestrated by Docker Compose on a single host, or by Helm on Kubernetes .
For a single-host deployment, the offline installer is the simplest path. Download the installer archive, extract it, and configure harbor.yml:
hostname: registry.example.com
harbor_admin_password: ChangeThisPassword
data_volume: /data/harbor
trivy:
ignore_unfixed: false
skip_update: false
The hostname is the address clients will use to reach Harbor. It must match the TLS certificate. The data_volume is where images and metadata are stored .
Harbor requires HTTPS by default. You generate a self-signed certificate for testing or use a CA-signed certificate for production:
openssl req -x509 -newkey rsa:4096 -nodes \
-keyout /data/cert/registry.key \
-out /data/cert/registry.crt \
-days 3650 \
-subj "/CN=registry.example.com" \
-addext "subjectAltName=DNS:registry.example.com"
Then update harbor.yml with the certificate paths and run the installer:
sudo ./install.sh --with-trivy
The --with-trivy flag enables the vulnerability scanner . After installation, Harbor is accessible at https://registry.example.com with the default credentials admin / ChangeThisPassword .
Harbor’s web UI provides project management, user and group administration, and repository browsing. Projects are the unit of isolation: each project has its own set of images, its own members, and its own access policies . A user can be a project administrator, a developer (push and pull), or a guest (pull only). This is the role-based access control that the Docker Registry server lacks entirely.
Harbor also supports image replication: you can configure a remote Harbor instance as a replication target, and Harbor will automatically copy images between them. This is used for multi-region deployments, where you want images close to the compute, or for disaster recovery, where a second Harbor instance holds a mirror of the first .
For Kubernetes deployments, Harbor is installed via Helm:
helm repo add harbor https://helm.goharbor.io
helm install harbor harbor/harbor \
--set expose.type=nodePort \
--set expose.tls.enabled=false \
--set persistence.persistentVolumeClaim.registry.storageClass=nfs-client
Running Harbor on Kubernetes gives you horizontal scaling of components, self-healing through pod restarts, and declarative configuration through the Helm chart .
c. Nexus Repository Manager: universal artifact storage
Nexus is a different kind of tool. Where Harbor is a purpose-built container registry, Nexus is a general-purpose artifact repository that happens to support Docker. It can host Maven, npm, PyPI, NuGet, Helm, and raw files alongside your container images .
The value proposition is consolidation. Instead of running one service for Docker images, another for Maven packages, and a third for npm modules, you run Nexus and point all your build tools at it. This simplifies infrastructure, reduces the number of services to maintain, and gives developers a single URL to remember .
Nexus runs as a single container:
docker run -d \
-p 8081:8081 \
-p 8881:8881 \
--name nexus \
-v /opt/nexus-data:/nexus-data \
sonatype/nexus3
Port 8081 is the web UI. Port 8881 is where the Docker registry will listen. The -v flag persists data across container restarts .
After starting Nexus, the admin password is in /nexus-data/admin.password inside the container:
docker exec -it nexus cat /nexus-data/admin.password
Log into the UI at http://localhost:8081 with username admin and that password .
Docker support is disabled by default in Nexus. You enable it in Security → Realms by activating the Docker Bearer Token Realm. Then you create a Blob Store for Docker image data—file-based storage is fine for internal deployments .
Next, create a docker-hosted repository. This is Nexus’s equivalent of a private registry. Configure it with a name (e.g., docker-hosted), an HTTP port (e.g., 8881), and select the blob store you just created. The Allow anonymous docker pull option controls whether unauthenticated users can pull images; leave it unchecked for private repositories .
Once configured, Docker clients need to know that the Nexus registry is allowed over HTTP. Add it to /etc/docker/daemon.json:
{
"insecure-registries": ["192.168.1.100:8881"]
}
Restart Docker . Then log in and push:
docker login 192.168.1.100:8881
docker tag my-app:latest 192.168.1.100:8881/my-app:latest
docker push 192.168.1.100:8881/my-app:latest
Nexus stores the image in the repository you configured. The same Nexus instance can simultaneously serve Maven artifacts on port 8081 and Docker images on port 8881, with separate access controls for each .
Complete Example Session
The following session walks through deploying the Docker Registry server with authentication and TLS via an Nginx reverse proxy, then pushing and pulling an image.
# ============================================
# PART 1: PREPARE DIRECTORIES
# ============================================
sudo mkdir -p /opt/registry/{data,auth,certs}
sudo chown -R $USER:$USER /opt/registry
# ============================================
# PART 2: CREATE AUTHENTICATION FILE
# ============================================
sudo apt install -y apache2-utils
htpasswd -Bc /opt/registry/auth/htpasswd admin
# Enter password when prompted
# ============================================
# PART 3: GENERATE TLS CERTIFICATE
# ============================================
openssl req -x509 -newkey rsa:4096 -nodes \
-keyout /opt/registry/certs/registry.key \
-out /opt/registry/certs/registry.crt \
-days 365 \
-subj "/CN=registry.example.com" \
-addext "subjectAltName=DNS:registry.example.com"
# ============================================
# PART 4: RUN THE REGISTRY WITH AUTH AND TLS
# ============================================
docker run -d \
--name registry \
--restart always \
-p 5000:5000 \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/auth:/auth \
-v /opt/registry/certs:/certs \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
-e "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt" \
-e "REGISTRY_HTTP_TLS_KEY=/certs/registry.key" \
registry:2
# ============================================
# PART 5: VERIFY THE REGISTRY IS RUNNING
# ============================================
docker ps
curl -k -u admin:password https://localhost:5000/v2/_catalog
# Expected: {"repositories":[]}
# ============================================
# PART 6: LOG IN AND PUSH AN IMAGE
# ============================================
docker login localhost:5000 -u admin
# Enter password
docker pull hello-world:latest
docker tag hello-world:latest localhost:5000/hello-world:v1
docker push localhost:5000/hello-world:v1
# ============================================
# PART 7: VERIFY THE IMAGE IS STORED
# ============================================
curl -k -u admin:password https://localhost:5000/v2/_catalog
# Expected: {"repositories":["hello-world"]}
# ============================================
# PART 8: PULL THE IMAGE FROM THE REGISTRY
# ============================================
docker rmi localhost:5000/hello-world:v1
docker pull localhost:5000/hello-world:v1
docker images | grep hello-world
# ============================================
# PART 9: INSPECT STORAGE ON HOST
# ============================================
ls /opt/registry/data/docker/registry/v2/repositories/
# Expected: hello-world
# ============================================
# PART 10: CLEAN UP (OPTIONAL)
# ============================================
docker stop registry
docker rm registry
# Data remains in /opt/registry/data
The ten parts cover the full lifecycle: directory preparation, authentication setup, TLS certificate generation, registry deployment with security enabled, verification, image push, catalog verification, image pull, storage inspection, and cleanup. What the session demonstrates is that a secure private registry requires three things—persistent storage, authentication, and TLS—and that the Docker Registry server provides the core functionality while leaving TLS termination and access control to surrounding infrastructure.
Quick Reference
Registry Comparison
| Feature | Docker Registry | Harbor | Nexus |
|---|---|---|---|
| Setup complexity | Minimal | Moderate | Moderate |
| Web UI | No (optional third-party) | Yes | Yes |
| Authentication | htpasswd (manual) | Built-in RBAC | Built-in RBAC |
| Vulnerability scanning | No | Yes (Trivy) | Plugin-dependent |
| Image replication | No | Yes | Yes |
| Multi-format support | No | Helm charts | Docker, Maven, npm, PyPI, Helm |
| Storage backends | Filesystem, S3 | Filesystem, S3, Azure Blob | Filesystem, S3, multiple drivers |
Registry Ports and Paths
| Registry | Default Web Port | Default Docker Port | Data Path |
|---|---|---|---|
| Docker Registry | 5000 | 5000 | /var/lib/registry |
| Harbor | 80/443 | 443 | /data/harbor |
| Nexus | 8081 | 8881 (configurable) | /nexus-data |
Client Configuration
| Setting | File | Purpose |
|---|---|---|
| Insecure registry | /etc/docker/daemon.json | Allow HTTP registry |
| Login credentials | ~/.docker/config.json | Stored after docker login |
| CA certificate | /etc/docker/certs.d/ | Trust registry TLS |
Push and Pull Commands
| Operation | Command |
|---|---|
| Tag for registry | docker tag app:latest registry:5000/app:latest |
| Push | docker push registry:5000/app:latest |
| Pull | docker pull registry:5000/app:latest |
| Login | docker login registry:5000 |
| List catalog | curl -u user:pass https://registry:5000/v2/_catalog |
Best Practices
✅ Do This:
docker run -d -p 5000:5000 -v /data:/var/lib/registry registry:2 # ✅ mount volume
docker tag app:latest localhost:5000/app:v1 # ✅ tag with registry
htpasswd -Bc /auth/htpasswd admin # ✅ bcrypt auth
docker login registry.example.com # ✅ authenticate first
❌ Don’t Do This:
docker run -d -p 5000:5000 registry:2 # ❌ no volume, data lost
docker push app:latest # ❌ goes to Docker Hub
docker run -p 5000:5000 --privileged registry:2 # ❌ unnecessary privilege
curl http://registry:5000/v2/_catalog # ❌ no auth over HTTP
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
server gave HTTP response to HTTPS client | Docker expects HTTPS | Add to insecure-registries or enable TLS |
| Data lost after container restart | No volume mounted | Mount /var/lib/registry |
| Push goes to Docker Hub | Image not tagged with registry prefix | Tag as registry:port/image:tag |
| Auth fails after restart | htpasswd file not persisted | Mount /auth directory |
| TLS certificate rejected | Self-signed not trusted | Add CA to /etc/docker/certs.d/ |
| Harbor install fails | Port 80/443 in use | Stop conflicting services or change ports |
Real-World Examples
1. Development registry (no auth, HTTP)
docker run -d -p 5000:5000 -v /data:/var/lib/registry registry:2
2. Production registry with htpasswd
docker run -d -p 5000:5000 -v /data:/var/lib/registry -v /auth:/auth \
-e REGISTRY_AUTH=htpasswd -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd registry:2
3. Harbor single-host deployment
./install.sh --with-trivy
4. Nexus Docker repository
Security → Realms → Activate Docker Bearer Token Realm
Repositories → Create → docker-hosted
5. Push to Harbor
docker tag app:latest harbor.example.com/library/app:latest
docker push harbor.example.com/library/app:latest
6. Pull from Nexus
docker pull nexus.example.com:8881/app:latest
7. Harbor replication
Administration → Registries → New Endpoint → Configure target Harbor
Projects → Replication → New Rule
8. TLS with Nginx reverse proxy
server {
listen 443 ssl;
server_name registry.example.com;
client_max_body_size 2G;
location / { proxy_pass http://localhost:5000; }
}
9. CA certificate for Docker client
sudo mkdir -p /etc/docker/certs.d/registry.example.com:5000
sudo cp registry.crt /etc/docker/certs.d/registry.example.com:5000/ca.crt
10. Kubernetes pull secret
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=admin --docker-password=password
Visual
Registry Deployment Options
┌──────────────────────────────────────────────┐
│ DOCKER REGISTRY (minimal) │
│ ┌────────────────────────────────────────┐ │
│ │ registry:2 container │ │
│ │ - HTTP API v2 │ │
│ │ - Filesystem storage │ │
│ │ - No UI, no auth (by default) │ │
│ └────────────────────────────────────────┘ │
│ │
│ HARBOR (enterprise) │
│ ┌────────────────────────────────────────┐ │
│ │ Harbor Core │ Harbor Portal │ │
│ │ Registry │ Job Service │ │
│ │ PostgreSQL │ Redis │ │
│ │ Trivy │ (all containers) │ │
│ └────────────────────────────────────────┘ │
│ │
│ NEXUS (multi-format) │
│ ┌────────────────────────────────────────┐ │
│ │ Nexus Repository Manager │ │
│ │ - Docker (port 8881) │ │
│ │ - Maven, npm, PyPI, Helm (port 8081) │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
Image Push Flow
┌──────────────────────────────────────────────┐
│ LOCAL MACHINE │
│ ┌─────────┐ │
│ │ my-app │ │
│ │ :latest │ │
│ └────┬────┘ │
│ │ docker tag │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ registry:5000/my-app:v1 │ │
│ └────────────┬────────────┘ │
│ │ docker push │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ REGISTRY (port 5000) │ │
│ │ /var/lib/registry/ │ │
│ │ docker/registry/v2/ │ │
│ │ repositories/ │ │
│ │ my-app/ │ │
│ └─────────────────────────┘ │
└──────────────────────────────────────────────┘
Registry Security Layers
┌──────────────────────────────────────────────┐
│ LAYER 1: TLS │
│ Encrypts traffic between client and registry│
│ Required for production │
│ │
│ LAYER 2: AUTHENTICATION │
│ Verifies identity before push/pull │
│ htpasswd (basic) or RBAC (Harbor/Nexus) │
│ │
│ LAYER 3: AUTHORIZATION │
│ Determines what authenticated users can do │
│ Push, pull, delete, admin (project-scoped) │
│ │
│ LAYER 4: SCANNING │
│ Detects vulnerabilities in images │
│ Trivy (Harbor), plugin-dependent (Nexus) │
└──────────────────────────────────────────────┘
When to Use Which
┌──────────────────────────────────────────────┐
│ NEED: Simple local image cache │
│ USE: Docker Registry │
│ │
│ NEED: Team registry with access control │
│ USE: Harbor │
│ │
│ NEED: One repository for Docker + Maven │
│ USE: Nexus │
│ │
│ NEED: Vulnerability scanning before deploy │
│ USE: Harbor (with Trivy) │
│ │
│ NEED: Air-gapped environment │
│ USE: Harbor (offline installer) │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Docker Registry | Minimal, single container, HTTP API v2 |
| Harbor | Enterprise, RBAC, Trivy scanning, replication |
| Nexus | Universal, multi-format, Docker + Maven + npm |
| Registry default port | 5000 |
| Harbor default ports | 80 (HTTP), 443 (HTTPS) |
| Nexus Docker port | Configurable (e.g., 8881) |
| Auth method (Registry) | htpasswd |
| Auth method (Harbor/Nexus) | Built-in RBAC |
| Storage path (Registry) | /var/lib/registry |
| Required for production | TLS + authentication |
Key takeaways:
- Start with the Docker Registry for simplicity. It is one container, one volume, one command. Add auth and TLS as needed .
- Harbor is the enterprise choice. It adds role-based access control, vulnerability scanning, image replication, and a web UI on top of the same Docker Distribution core .
- Nexus consolidates artifact storage. If you already use Nexus for Maven or npm, enabling Docker support is a configuration change, not a new service .
- Authentication is mandatory for production. The Docker Registry server has no built-in auth; Harbor and Nexus provide RBAC out of the box .
- TLS is not optional. Docker clients refuse to connect to HTTP registries unless explicitly configured otherwise. For anything beyond localhost, use HTTPS .
- Volume mounts are essential. Without a persistent volume, all images are lost when the registry container is removed .
- The image tag determines the registry.
docker push app:latestgoes to Docker Hub.docker push registry:5000/app:latestgoes to your registry. Retag before pushing . - Air-gapped environments need the offline installer. Harbor provides one; it is the standard approach for isolated networks .
Remember: A private container registry is not a luxury—it is infrastructure. The Docker Registry server gives you the minimal viable registry in a single command. Harbor turns that into an enterprise platform with security, scanning, and replication. Nexus turns it into one piece of a universal artifact repository. The choice depends on what you need: a simple cache, a secured registry, or a single destination for all your build artifacts. Every path starts the same way: a container, a volume, a port, and the decision to make it yours.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!