Private Image Repository
Don't want to push your company's code to the public Docker Hub? Set up a private image repository in 5 minutes.
1. What You'll Learn
- Deployment of Registry Images
- Mirror Push and Pull Operations
- Repository Authentication Configuration
- Mirror Cleanup and Garbage Collection
- Approaches to Integrating Repositories with CI/CD
2. A True Story of a Team Leader
(1) Pain Point: Code cannot be pushed to a public repository
Alice doesn’t want to push the company’s code to the public Docker Hub—the images might contain sensitive information such as configuration files and default environment variable values. However, the team needs an image repository to share build artifacts; manually docker save/load transferring them is too inefficient—it takes five developers 30 minutes each time to pass an image along.
(2) Solutions for Private Registries
I set up a private repository with authentication using just three lines of configuration.
# Start a private registry with authentication
docker run -d -p 5000:5000 \
-v registry-data:/var/lib/registry \
--restart=always \
--name registry registry:2
(3) Benefits: 5-minute image sharing
Teams can start using private repositories within 5 minutes. docker push Once, and everyone docker pull is set. Image delivery time has been reduced from 30 minutes to 30 seconds.
3. Comparison of Mirror Repository Solutions
(1) Comparison of the Three Options
| Dimension | Docker Hub (Public) | Private Registry | Harbor |
|---|---|---|---|
| Cost | Free with limits; paid plan $5/month | Completely free | Free and open source |
| Authentication | Docker Account | htpasswd/TLS | LDAP/OIDC/DB |
| Web UI | ✅ | ❌ | ✅ |
| Image Scanning | ✅ (Paid) | ❌ | ✅ (Trivy integration) |
| Deployment Difficulty | None (Already live) | Low (1 command) | Medium (Docker Compose) |
| Suitable Scale | Individuals/Small Teams | Small to Medium-Sized Teams | Enterprise |
(2) Image Naming Conventions
# Official images on Docker Hub
docker.io/library/nginx:1.25
# User images on Docker Hub
docker.io/myorg/myapp:v2.0
# Private registry images
localhost:5000/myapp:v2.0
registry.example.com/team/app:v1.0
4. Deploying a Private Registry
▶ Example: Starting the Registry Container (Difficulty: ⭐)
# Start a basic private registry
docker run -d \
-p 5000:5000 \
--name registry \
--restart=always \
-v registry-data:/var/lib/registry \
registry:2
# Verify it's running
curl http://localhost:5000/v2/_catalog
{"repositories":[]}
(1) Data Persistence
| Method | Command | Description |
|---|---|---|
| Named Volume | -v registry-data:/var/lib/registry |
Docker-managed persistent storage |
| Bind Mount | -v /data/registry:/var/lib/registry |
Specify host directory |
| Default (without -v) | No persistence | Image data is lost when the container is deleted |
5. Pushing and Pulling Images
▶ Example: Pushing to a private repository (Difficulty: ⭐⭐)
# 1. Tag an image for the private registry
docker tag nginx:1.25-alpine localhost:5000/web/nginx:v1
# 2. Push the image
docker push localhost:5000/web/nginx:v1
# 3. Verify it's in the registry
curl http://localhost:5000/v2/web/nginx/tags/list
{"name":"web/nginx","tags":["v1"]}
▶ Example: Pulling an image from a private repository (Difficulty: ⭐⭐)
# 1. Remove the local copy
docker rmi localhost:5000/web/nginx:v1
# 2. Pull from the private registry
docker pull localhost:5000/web/nginx:v1
# 3. Run the pulled image
docker run --rm localhost:5000/web/nginx:v1 nginx -v
nginx version: nginx/1.25.3
6. Repository Authentication Configuration
By default, the registry is not authenticated, so anyone can push or pull. Authentication must be configured for production environments.
▶ Example: Generating an htpasswd password file (Difficulty: ⭐⭐⭐)
# Create a directory for auth data
mkdir -p auth
# Generate htpasswd file (requires httpd-tools / apache2-utils)
htpasswd -Bbn admin secret123 > auth/htpasswd
# Start registry with authentication
docker run -d \
-p 5000:5000 \
--name registry-auth \
--restart=always \
-v registry-data:/var/lib/registry \
-v $(pwd)/auth:/auth \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
registry:2
(1) Post-Authentication Operations
# Login to the private registry
docker login localhost:5000
# Username: admin
# Password: secret123
# Push (requires login)
docker push localhost:5000/myapp:v1
# Pull (requires login)
docker pull localhost:5000/myapp:v1
# Logout
docker logout localhost:5000
7. View the repository directory
▶ Example: Use curl to view the catalog and tags (Difficulty: ⭐⭐)
# List all repositories
curl -s http://localhost:5000/v2/_catalog | python3 -m json.tool
# List tags for a specific repository
curl -s http://localhost:5000/v2/web/nginx/tags/list | python3 -m json.tool
# View image manifest (detailed metadata)
curl -s -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
http://localhost:5000/v2/web/nginx/manifests/v1 | python3 -m json.tool
# catalog
{
"repositories": [
"web/nginx",
"team/api"
]
}
# tags/list
{
"name": "web/nginx",
"tags": ["v1", "v2"]
}
8. Garbage Collection
After an image in the Registry is deleted (untagged), the layer data is not automatically cleaned up. You must manually perform garbage collection.
▶ Example: Garbage Collection (Difficulty: ⭐⭐⭐)
# Delete a tag (mark for deletion)
curl -X DELETE -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
http://localhost:5000/v2/web/nginx/manifests/<digest>
# Run garbage collection inside the registry container
docker exec registry bin/registry garbage-collect \
/etc/docker/registry/config.yml
# Or with dry-run first (preview only)
docker exec registry bin/registry garbage-collect --dry-run \
/etc/docker/registry/config.yml
9. HTTP vs. HTTPS Repositories
(1) Docker Security Policies
By default, Docker only allows HTTPS connections to the registry (except for localhost). Remote registries must be configured with HTTPS, or the insecure-registries option must be added on the client side.
| Scenario | Security Level | Configuration |
|---|---|---|
localhost:5000 |
HTTP allowed by default | No additional configuration required |
| Remote HTTP Repository | Insecure, requires explicit permission | insecure-registries |
| Remote HTTPS Repository | ✅ Recommended | TLS Certificate |
(2) Configure insecure-registries (for test environments only)
{
"insecure-registries": ["registry.example.com:5000"]
}
10. Complete Example: Deploying a Private Repository with Authentication
# ============================================
# Complete walkthrough: Private registry with auth
# Covers: deploy, auth, push, pull, catalog, GC
# ============================================
# 1. Create directories for persistent data
mkdir -p auth registry-data
# 2. Generate htpasswd authentication file
htpasswd -Bbn admin secret123 > auth/htpasswd
# 3. Start the registry with authentication and persistence
docker run -d \
-p 5000:5000 \
--name my-registry \
--restart=always \
-v $(pwd)/registry-data:/var/lib/registry \
-v $(pwd)/auth:/auth \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
registry:2
# 4. Login to the registry
docker login localhost:5000 -u admin -p secret123
# 5. Tag and push an image
docker tag nginx:1.25-alpine localhost:5000/web/nginx:v1.0
docker push localhost:5000/web/nginx:v1.0
# 6. Verify the push
curl -s -u admin:secret123 http://localhost:5000/v2/_catalog
curl -s -u admin:secret123 http://localhost:5000/v2/web/nginx/tags/list
# 7. Pull from another machine (after login)
docker pull localhost:5000/web/nginx:v1.0
# 8. Clean up
docker logout localhost:5000
# curl catalog
{"repositories":["web/nginx"]}
# curl tags
{"name":"web/nginx","tags":["v1.0"]}
❓ FAQ
registry garbage-collect to free up unreferenced layer data. We recommend writing a script to periodically clean up old versions that are more than N days old. Harbor has built-in cleanup policies.📖 Summary
registry:2Mirror: Launch a private repository with a single command; data must be persisted using a volume- Mirror naming convention:
registry-host/repo:tag; you must tag before pushing - The repository is protected by htpasswd authentication; you must run
docker loginbefore you can push or pull. curl /v2/_catalogand/v2/repo/tags/listView repository contents- Garbage collection (garbage-collect) frees up layer data from deleted images
- HTTPS is required in production environments; HTTP is limited to localhost or test environments.
📝 Exercises
- Basic Exercise (Difficulty ⭐): Start a private registry using
docker run, pushnginx:alpinetolocalhost:5000/test/nginx:v1, and then pull it from another machine (or after deleting the local image) to verify. - Advanced Problem (Difficulty: ⭐⭐): Configure htpasswd authentication so that pushes are rejected when not logged in, but succeed after logging in.
- Challenge (Difficulty: ⭐⭐⭐): Use an Nginx reverse proxy to configure HTTPS for the Registry (a self-signed certificate can be used), and access the repository via
https://registry.local/v2/_catalog.



