Set Up Private Docker Registry on VPS
Docker Hub has rate limiting constraints and privacy concerns, especially for internal project images you don't want exposed to the public. Setting up a private Docker Registry on your VPS allows you to push and pull images faster since they stay within your own network, and you gain complete access control through authentication and TLS encryption.
Install Docker Registry with Docker Compose
The easiest approach is to run Registry via Docker Compose and place an Nginx reverse proxy in front:
sudo apt update && sudo apt install docker.io docker-compose -y
sudo systemctl enable --now docker
mkdir -p ~/registry/auth ~/registry/data
cd ~/registry
cat > docker-compose.yml <<'EOF'
version: '3'
services:
registry:
image: registry:2
container_name: docker-registry
restart: unless-stopped
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
volumes:
- ./auth:/auth
- ./data:/data
ports:
- "5000:5000"
EOF
Create User Authentication
# Install htpasswd
sudo apt install apache2-utils -y
# Create user (replace myuser with your desired username)
htpasswd -Bc ~/registry/auth/htpasswd myuser
# You will be prompted for a password
# Add additional users
htpasswd -B ~/registry/auth/htpasswd anotheruser
Configure Nginx Reverse Proxy + TLS
sudo nano /etc/nginx/sites-available/registry
server {
listen 443 ssl;
server_name registry.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/registry.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.yourdomain.com/privkey.pem;
client_max_body_size 0; # Critical: no size limit for images
chunked_transfer_encoding on;
location / {
proxy_pass http://127.0.0.1:5000;
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 $scheme;
proxy_read_timeout 900;
}
}
server {
listen 80;
server_name registry.yourdomain.com;
return 301 https://$host$request_uri;
}
sudo ln -s /etc/nginx/sites-available/registry /etc/nginx/sites-enabled/
sudo certbot --nginx -d registry.yourdomain.com
sudo nginx -t && sudo systemctl reload nginx
cd ~/registry && docker-compose up -d
Push and Pull Images from Your Private Registry
# Log in from your client machine
docker login registry.yourdomain.com
# Enter the username and password you created with htpasswd
# Tag the image before pushing
docker tag myapp:latest registry.yourdomain.com/myapp:latest
# Push to Registry
docker push registry.yourdomain.com/myapp:latest
# Pull it back for use
docker pull registry.yourdomain.com/myapp:latest
View Images in Your Registry
# List all repositories
curl -u myuser:password https://registry.yourdomain.com/v2/_catalog
# View tags for an image
curl -u myuser:password https://registry.yourdomain.com/v2/myapp/tags/list
# Check disk space usage
du -sh ~/registry/data/
client_max_body_size 0 in your Nginx config. Without it, Nginx will reject images larger than 1MB, which applies to virtually every real-world image.
Verify Docker Registry is Working Correctly
# Check container status
docker ps | grep registry
# Test the API directly
curl -u myuser:password https://registry.yourdomain.com/v2/
# Push a test image: pull from Docker Hub, tag it, and push to your private registry
docker pull hello-world
docker tag hello-world registry.yourdomain.com/hello-world:test
docker push registry.yourdomain.com/hello-world:test
# Pull it back to verify
docker rmi registry.yourdomain.com/hello-world:test
docker pull registry.yourdomain.com/hello-world:test
/v2/ and get {} back with HTTP 200, your Registry is working correctly and authentication passed.
Common Issues & Troubleshooting
Error: "http: server gave HTTP response to HTTPS client"
Your Docker client is trying to connect via HTTPS, but Registry is running on plain HTTP. You need to either configure insecure-registries or enable TLS:
sudo nano /etc/docker/daemon.json
{
"insecure-registries": ["registry.yourdomain.com:5000"]
}
sudo systemctl restart docker
Error: "413 Request Entity Too Large"
Nginx rejected the image because of body size limits. Update the config:
sudo nano /etc/nginx/sites-available/registry
# Add to the server block:
client_max_body_size 0;
sudo nginx -t && sudo systemctl reload nginx
Error: "unauthorized: authentication required"
Either your htpasswd file is incorrect or the mount path is wrong. Verify:
# Check the htpasswd file
cat ~/registry/auth/htpasswd
# Test login
docker logout registry.yourdomain.com
docker login registry.yourdomain.com
# View container logs
docker logs docker-registry
Disk Full from Old Images
You need to garbage collect and remove unreferenced layers:
docker exec docker-registry registry garbage-collect /etc/docker/registry/config.yml
# Check size before and after
du -sh ~/registry/data/
Frequently Asked Questions
How does a private Registry differ from Docker Hub?
Docker Hub is a public registry with rate limiting (100 pulls per 6 hours for anonymous users). A private Registry on your VPS has no such restrictions on push/pull operations, you control access completely, and your images are never exposed to the public. It's ideal for internal CI/CD pipelines within your organization.
Can I use Registry with Kubernetes?
Yes. You need to create an imagePullSecret in Kubernetes first, then reference it in your Pod spec or ServiceAccount so Kubernetes can pull images from your private registry. Use kubectl create secret docker-registry to set it up.
Are there alternatives to the official Registry image?
Yes. Options include Harbor (enterprise with UI and vulnerability scanning), Gitea Package Registry, and Nexus Repository Manager. However, for single-VPS private use, the official Registry image is lightweight and rock-solid.