Docker

How to Set Up a Self-Hosted Docker Registry: A Complete Beginner's Guide

Step-by-step guide to setting up a private Docker registry with authentication and TLS. Perfect for homelabs - keep your container images private and avoid Docker Hub rate limits.

AU

Author

David Okonkwo

Disclosure: This article contains affiliate links. If you purchase through these links, we may earn a commission at no additional cost to you. We only recommend products we have personally tested or thoroughly researched.

Key Takeaways

  • A self-hosted Docker registry gives you full control over your container images - no rate limits, no privacy concerns, no vendor lock-in
  • Setting up a basic registry with Docker Compose takes under 30 minutes - even if you're new to Docker
  • You need three things: storage for images, authentication for security, and TLS certificates for encrypted transfers
  • Use named volumes (not bind mounts) for registry data to ensure reliable performance and easy backups
  • A self-hosted registry is ideal for homelabs, small teams, and anyone who wants to keep their images private

If you've been building Docker containers for your homelab, you've probably pushed images to Docker Hub at some point. It works fine for public projects, but what happens when you want to keep your custom configurations private? Or when you hit Docker Hub's rate limits during a deployment?

A self-hosted Docker registry solves both problems. You get a private, fast, and completely controlled place to store your container images. And the best part - it's easier to set up than you might think.

In this guide, I'll walk you through setting up your own Docker registry from scratch. We'll cover everything from the basic setup to adding authentication and TLS encryption. By the end, you'll have a production-ready registry running on your homelab.

Why Self-Host a Docker Registry?

Before we jump into the setup, let me explain why this matters for your homelab.

Docker Hub is convenient, but it comes with limitations:

  • Rate limits - Docker Hub limits anonymous users to 100 pulls per 6 hours and authenticated users to 200 pulls per 6 hours. If you're running multiple services that pull images regularly, you'll hit these limits faster than you think.
  • Privacy concerns - Anything you push to Docker Hub's free tier is public. Your custom Nginx configuration with your specific security settings? Public. Your application with your API keys baked in (please don't do this)? Public.
  • No control over uptime - When Docker Hub has an outage, your deployments fail. With a self-hosted registry, you control the availability.
  • No vendor lock-in - If Docker Hub changes their pricing or terms, you're not affected.

A self-hosted registry gives you:

  • Unlimited pulls - No rate limits to worry about
  • Complete privacy - Your images stay on your network
  • Faster pulls - Local network speeds instead of internet bandwidth
  • Full control - You decide who can push and pull images

What You'll Need

Before we start, make sure you have:

  • A Linux server (Ubuntu 22.04 or 24.04 recommended) with Docker installed
  • At least 2GB of free storage for images (more if you plan to store many)
  • Basic familiarity with Docker commands (if you can run docker run, you're good)
  • A domain name pointing to your server (for TLS setup) - optional but recommended

Don't have Docker installed yet? Check out our Docker for Homelabs guide to get started.

Step 1: Create the Directory Structure

First, let's create the directories our registry will use. SSH into your server and run:

sudo mkdir -p /opt/registry/data
sudo mkdir -p /opt/registry/auth
sudo mkdir -p /opt/registry/certs
sudo chown -R $USER:$USER /opt/registry

Here's what each directory does:

  • /opt/registry/data - Where your Docker images are stored
  • /opt/registry/auth - Authentication credentials
  • /opt/registry/certs - TLS certificates for HTTPS

Using /opt/registry keeps things organized and makes backups straightforward. You can use any location you prefer, just update the paths in the Docker Compose file accordingly.

Step 2: Set Up Authentication

Without authentication, anyone who can reach your registry can push or pull images. That's a security risk we need to address.

We'll use Apache's htpasswd utility to create a password file:

sudo apt install apache2-utils -y
htpasswd -Bc /opt/registry/auth/htpasswd admin

When prompted, enter a strong password. This creates the user "admin" - change this to whatever username you prefer.

To add more users later:

htpasswd -B /opt/registry/auth/htpasswd anotheruser

The -B flag uses bcrypt encryption, which is more secure than the default MD5. The -c flag creates a new file - only use it for the first user.

Why this matters: Authentication prevents unauthorized access to your images. Without it, anyone on your network (or the internet, if your port is exposed) could push malicious images to your registry.

Step 3: Generate TLS Certificates

Docker clients refuse to communicate with registries over plain HTTP by default (and for good reason - you don't want your images intercepted in transit). We need TLS certificates.

You have two options:

Option A: Self-Signed Certificates (Quick Setup)

For homelabs or development environments, self-signed certificates work fine:

openssl req -newkey rsa:4096 -nodes -sha256 \
  -keyout /opt/registry/certs/domain.key \
  -x509 -days 365 \
  -out /opt/registry/certs/domain.crt \
  -subj "/CN=registry.yourdomain.com"

Replace registry.yourdomain.com with your actual domain or IP address.

Option B: Let's Encrypt (Production Setup)

If your registry is accessible from the internet and you have a domain name, use Let's Encrypt for trusted certificates:

sudo apt install certbot -y
sudo certbot certonly --standalone -d registry.yourdomain.com
sudo cp /etc/letsencrypt/live/registry.yourdomain.com/fullchain.pem /opt/registry/certs/domain.crt
sudo cp /etc/letsencrypt/live/registry.yourdomain.com/privkey.pem /opt/registry/certs/domain.key

With self-signed certificates, you'll need to trust the certificate on each Docker client machine. We'll cover that in Step 5.

Step 4: Create the Docker Compose File

Now let's put it all together with a Docker Compose file:

cat > /opt/registry/docker-compose.yml << 'EOF'
version: '3.8'

services:
  registry:
    image: registry:2
    container_name: docker-registry
    restart: always
    ports:
      - "5000:5000"
    environment:
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm"
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
      REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
      REGISTRY_HTTP_TLS_KEY: /certs/domain.key
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
    volumes:
      - ./data:/var/lib/registry
      - ./auth:/auth
      - ./certs:/certs
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "https://localhost:5000/v2/"]
      interval: 30s
      timeout: 10s
      retries: 3
EOF

Let me break down the important settings:

  • REGISTRY_AUTH: htpasswd - Enables username/password authentication
  • REGISTRY_STORAGE_DELETE_ENABLED: "true" - Allows you to delete old images to save space
  • healthcheck - Monitors registry health and restarts if it becomes unresponsive

Why named volumes vs bind mounts here? For the registry data directory, we're using a bind mount (./data:/var/lib/registry) intentionally. Since we want direct access to the registry data for backups and management, bind mounts give us that flexibility. However, for most other Docker use cases, named volumes are the better choice.

Step 5: Start the Registry

Navigate to your registry directory and start the container:

cd /opt/registry
docker compose up -d

Verify it's running:

docker compose ps
docker logs docker-registry

You should see output indicating the registry is listening on port 5000. If you see any errors, check that:

  • The certificates exist at the paths specified
  • The htpasswd file exists and is readable
  • Port 5000 isn't already in use

Test the registry endpoint:

curl -u admin:yourpassword https://localhost:5000/v2/

If everything is working, you'll get an empty JSON response {}. That's the Docker registry API confirming it's ready.

Step 6: Configure Docker Clients

Now we need to tell Docker clients to trust your registry. The approach depends on whether you're using self-signed or Let's Encrypt certificates.

For Self-Signed Certificates

On each client machine that will use the registry, you need to trust the certificate:

# Create the certificate directory
sudo mkdir -p /etc/docker/certs.d/registry.yourdomain.com:5000

# Copy the certificate
sudo cp /opt/registry/certs/domain.crt /etc/docker/certs.d/registry.yourdomain.com:5000/ca.crt

# Restart Docker
sudo systemctl restart docker

Replace registry.yourdomain.com with your registry's address. This tells Docker to trust your certificate for this specific registry.

For Let's Encrypt Certificates

If you're using Let's Encrypt, no additional configuration is needed - Docker trusts Let's Encrypt certificates by default.

Log In to the Registry

On each client machine:

docker login registry.yourdomain.com:5000

Enter your username and password when prompted. Docker saves these credentials, so you only need to do this once per machine.

Step 7: Push Your First Image

Let's test the full workflow by pushing an image to your registry:

# Pull a test image
docker pull nginx:latest

# Tag it for your registry
docker tag nginx:latest registry.yourdomain.com:5000/nginx

# Push it
docker push registry.yourdomain.com:5000/nginx

If everything works, the image is now stored in your private registry. You can pull it from any authenticated client:

docker pull registry.yourdomain.com:5000/nginx

Why this matters: This is the core workflow - tag, push, pull. Once you have this working, you can use your registry for all your custom images.

Step 8: Clean Up Old Images

Over time, your registry will accumulate old image layers. Here's how to clean them up:

First, find the digest of an image you want to delete:

curl -u admin:yourpassword https://registry.yourdomain.com:5000/v2/nginx/tags/list

Then delete it using the registry API:

# Get the digest
DIGEST=$(curl -sI -u admin:yourpassword \
  -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
  https://registry.yourdomain.com:5000/v2/nginx/manifests/latest | grep -i Docker-Content-Digest | awk '{print $2}' | tr -d '\r')

# Delete the image
curl -u admin:yourpassword -X DELETE \
  https://registry.yourdomain.com:5000/v2/nginx/manifests/$DIGEST

Note that deleting the manifest doesn't immediately free up disk space - Docker garbage collection handles that. Run the garbage collector with:

docker exec docker-registry bin/registry garbage-collect /etc/docker/registry/config.yml

This is a good task to automate with a scheduled backup strategy.

Step 9: Set Up Automated Cleanup

For a production registry, you'll want automated cleanup. Create a simple script:

cat > /opt/registry/cleanup.sh << 'EOF'
#!/bin/bash
# Cleanup old images older than 30 days
REGISTRY="registry.yourdomain.com:5000"
AUTH="admin:yourpassword"

# List all repositories
REPOS=$(curl -s -u $AUTH https://$REGISTRY/v2/_catalog | jq -r '.repositories[]')

for REPO in $REPOS; do
  # List tags
  TAGS=$(curl -s -u $AUTH https://$REGISTRY/v2/$REPO/tags/list | jq -r '.tags[]' 2>/dev/null)
  
  for TAG in $TAGS; do
    # Check creation date
    CREATED=$(curl -sI -u $AUTH \
      -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
      https://$REGISTRY/v2/$REPO/manifests/$TAG | grep -i Docker-Content-Digest)
    
    # Add your date logic here to delete old images
  done
done
EOF
chmod +x /opt/registry/cleanup.sh

Add this to a cron job to run weekly:

sudo crontab -e
# Add: 0 2 * * 0 /opt/registry/cleanup.sh

Common Mistakes to Avoid

Here are the pitfalls I see most often when people set up their first Docker registry:

1. Using HTTP Instead of HTTPS

Docker clients refuse to pull from HTTP registries (except localhost). If you skip TLS setup, you'll get http: server gave HTTP response to HTTPS client errors. Always set up TLS, even with self-signed certificates.

2. Forgetting to Trust Self-Signed Certs

If you use self-signed certificates but don't add them to /etc/docker/certs.d/, you'll see x509: certificate signed by unknown authority errors. This is the most common setup mistake.

3. Not Enabling Deletion

Without REGISTRY_STORAGE_DELETE_ENABLED: "true", you can't delete old images. Your storage will fill up eventually, and you'll have to delete the entire registry data directory to reclaim space.

4. Using Weak Passwords

Your registry might be accessible from your entire network. Use strong passwords and consider adding fail2ban to block brute force attempts.

5. Not Backing Up Registry Data

The registry data in /opt/registry/data contains all your images. If you lose this data, you lose all your images. Include this directory in your backup strategy.

Recommended Gear

If you're setting up a Docker registry for your homelab, here are some hardware recommendations that will give you reliable performance:

  • Synology DiskStation DS224+ - Perfect for storing registry data with built-in RAID redundancy and easy backups
  • Samsung 870 EVO 2TB SSD - Fast storage for your registry data directory, significantly faster than HDDs for image layer storage
  • APC UPS BE600M1 - Protect your registry from power interruptions that could corrupt image data

Scaling Your Registry

Once you have the basic setup working, you might want to add more features:

Garbage Collection Automation

Set up a cron job to run garbage collection automatically:

# Add to crontab
0 3 * * * docker exec docker-registry bin/registry garbage-collect /etc/docker/registry/config.yml && docker restart docker-registry

Multiple Storage Backends

For larger setups, consider using S3-compatible storage instead of local filesystem:

REGISTRY_STORAGE: s3
REGISTRY_STORAGE_S3_ACCESSKEY: your-access-key
REGISTRY_STORAGE_S3_SECRETKEY: your-secret-key
REGISTRY_STORAGE_S3_BUCKET: your-registry-bucket
REGISTRY_STORAGE_S3_REGION: us-east-1

This is useful if you're running multiple registry instances or need better durability.

Web UI for Image Management

For a more user-friendly experience, consider adding a web UI like Docker Registry UI or docker-registry-frontend. These give you a visual interface to browse and manage your images.

Integrating with Your Homelab

Now that your registry is running, here's how to make it part of your daily workflow:

Update Your Docker Compose Files

Change your service images from Docker Hub to your registry:

# Before
image: nginx:latest

# After
image: registry.yourdomain.com:5000/nginx:latest

Push Custom Images Automatically

Add a build and push step to your CI/CD pipeline:

# Build your custom image
docker build -t registry.yourdomain.com:5000/myapp:v1.0 .

# Push to your registry
docker push registry.yourdomain.com:5000/myapp:v1.0

This workflow integrates well with Docker Compose best practices for managing multiple services.

What to Learn Next

Now that you have a working Docker registry, here are some topics to explore:

  • Docker Compose Best Practices - Learn how to organize your compose files for better maintainability
  • Docker Security Hardening - Secure your entire Docker setup, not just the registry
  • Docker Monitoring with Prometheus - Track your registry's usage and performance
  • Container Orchestration - When you're ready to scale beyond single-host setups

Frequently Asked Questions

How much storage do I need for a Docker registry?

Start with at least 50GB for a small homelab registry. Each Docker image typically uses 100MB-2GB depending on the base image and layers. With regular cleanup, 50GB can store dozens of images.

Can I mirror Docker Hub images in my private registry?

Yes. You can pull images from Docker Hub and push them to your registry using docker tag and docker push. For automated mirroring, tools like Harbor or Docker Registry Manager can sync images from multiple upstream registries.

Is a self-hosted registry secure enough for production?

A properly configured registry with TLS and authentication is secure for most use cases. For enterprise environments, consider adding vulnerability scanning with tools like Trivy or Clair, and implement RBAC with Harbor for more granular access control.

What happens if my registry server goes down?

Docker clients cache images locally, so existing containers continue running. However, you won't be able to pull new images or push updates until the registry is back. This is why backups and monitoring are essential.

Can I use a Docker registry with Kubernetes?

Absolutely. You can configure Kubernetes to pull images from your private registry by creating an imagePullSecret. This is common in homelab Kubernetes setups like K3s or MicroK8s.