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:

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.

Terminal output showing a successful curl command verifying the SSL certificate of the private docker registry
Terminal output showing a successful curl command verifying the SSL certificate of the private docker registry

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

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.