In my early days of deploying apps, I relied heavily on Docker Hub. It worked great until I started building proprietary internal tools for clients. Suddenly, the idea of my source-code-adjacent images sitting on a third-party cloud felt risky, and the cost of private repositories started adding up.
If you’re wondering how to host a private docker registry, you’re likely looking for more control over your images, faster pull speeds within your own network, or simply a way to avoid monthly subscription fees. In this guide, I’ll walk you through setting up a self-hosted registry using the official Docker Registry image, securing it with Nginx and Let’s Encrypt, and integrating it into your workflow.
Prerequisites
Before we dive in, make sure you have the following ready:
- A Linux VPS (Ubuntu 22.04 is my recommendation).
- A domain or subdomain (e.g.,
registry.yourdomain.com) pointed to your server’s IP. - Docker and Docker Compose installed on the server.
- Basic familiarity with the terminal.
Step 1: Setting Up the Registry Container
The simplest way to start is by using the official registry image. I prefer using Docker Compose because it makes managing environment variables and volume persistence much cleaner.
Create a directory for your registry and a docker-compose.yml file:
mkdir docker-registry && cd docker-registry
touch docker-compose.yml
Add the following configuration to your file:
version: '3.8'
services:
registry:
image: registry:2
ports:
- "5000:5000"
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
volumes:
- ./data:/var/lib/registry
restart: always
Run the container with docker compose up -d. At this point, your registry is running, but it’s insecure (HTTP) and open to the public. We need to fix that immediately.
Step 2: Securing the Registry with Nginx and SSL
Docker will refuse to push to a registry over HTTP unless you explicitly configure “insecure registries” on every single client machine—which is a nightmare. The professional way to handle this is by placing Nginx in front as a reverse proxy with an SSL certificate.
I recommend using Certbot for Let’s Encrypt certificates. Once you have your certificates, configure Nginx to handle the traffic. Here is the snippet I use in my production setups:
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;
# Docker images can be large, so increase the client body size
client_max_body_size 0;
location / {
proxy_pass http://localhost: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;
}
}
As shown in the image below, ensuring your SSL handshake is successful is the most critical part of the setup, as Docker clients strictly enforce HTTPS for remote registries.
Step 3: Adding Authentication
You don’t want the entire internet pushing images to your server. I use htpasswd to create a simple basic authentication layer.
First, install apache2-utils and create a password file:
sudo apt-get install apache2-utils
htpasswd -Bc ./auth myusername
Now, update your docker-compose.yml to tell the registry to use this file:
services:
registry:
...
environment:
REGISTRY_AUTH:
htpasswd.realm: Registry
htpasswd.path: /auth/htpasswd
volumes:
- ./data:/var/lib/registry
- ./auth:/auth
Restart your container: docker compose up -d.
Step 4: Testing Your Private Registry
Now let’s test the full loop: Login, Tag, and Push.
# 1. Log in to your registry
docker login registry.yourdomain.com
# 2. Tag an existing image (or build a new one)
docker tag my-app:latest registry.yourdomain.com/my-app:v1
# 3. Push the image
docker push registry.yourdomain.com/my-app:v1
If you see the layers uploading and a final “Pushed” message, you’ve successfully hosted your own private registry!
Pro Tips for Registry Management
- Storage Backups: Your images live in the
./datafolder. I suggest using a cron job to rsync this folder to an S3 bucket or another server. - Automated Deployment: Now that you have a private hub, you can streamline your GitHub Actions for Docker deployment to push images directly to your VPS.
- VPS Optimization: If you are hosting multiple sites on one VPS using Docker, ensure you limit the registry’s memory usage in Docker Compose to prevent it from starving your web apps during large image pushes.
- Alternative Panels: If managing raw YAML feels tedious, look into Coolify vs Dokku comparison to see if a PaaS-like dashboard fits your workflow better.
Troubleshooting Common Issues
Error: “server gave HTTP response to HTTPS client”
This usually means Nginx isn’t configured correctly or your SSL certificate has expired. Double-check your Nginx logs using tail -f /var/log/nginx/error.log.
Error: “request entity too large”
This happens when Nginx limits the upload size. Ensure client_max_body_size 0; is present in your server block.
What’s Next?
Now that your registry is live, the next logical step is automating the cleanup. Docker registries don’t delete old images by default; they just create new layers. Look into the registry garbage-collect command to reclaim disk space periodically.
Ready to scale? If you’re managing a fleet of servers, consider moving your storage backend from local files to an S3-compatible store like MinIO.