New: run your homelab, smart home and subscriptions from Notion. All 3 templates for $29, not $42Save $13GET ALL THREE

Homelab Addiction

Self-Hosted Docker Registry Setup: Add Private Images, Auth, and TLS Without Guesswork

Set up a private Docker registry with persistent storage, authentication, TLS, and client trust steps so image pushes and pulls stay predictable inside a homelab.

HomelabAddiction Research Desk11 min read
Disclosure: This article may contain affiliate links. If you purchase through these links, we may earn a commission at no additional cost to you. Product references and operational guidance on this site should be treated as research-led unless the article explicitly documents hands-on testing evidence.
Documentation basis: This guide is grounded in the Docker Distribution deployment documentation, the registry configuration reference, the Docker certificate trust documentation, and the Docker Compose documentation. No hands-on timing claim is treated as evidence here unless the article documents the exact environment.

A private registry is useful when the homelab needs predictable image pulls, private custom builds, or lower dependence on upstream rate limits: it is not mandatory for every Docker stack, but it becomes worthwhile once more than one host or deployment path depends on the same images.

The critical decisions are straightforward: choose persistent storage, require authentication, terminate TLS correctly, and make sure every client host trusts the registry before the first production push.

This page helps a homelab operator build that path from documented registry behavior so pushes, pulls, and cleanup jobs stay predictable instead of becoming an emergency during the next redeploy.

Request flow for pushing and pulling images through a private Docker registry with authentication and TLS
Registry request path: clients tag and authenticate first, then push or pull image layers over HTTPS to a registry that stores the content on persistent local storage.

Why Self-Host a Docker Registry?

Before the setup begins, it helps to define when a private registry is worth the extra moving parts in a 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.

transmission signup

article_topic // Self-Hosted Docker Registry Setup

Don't leave without the setup notes.

Get practical homelab guides, failure logs, and beginner-friendly build notes in your inbox.

Enter your email address to receive the Homelab Addiction newsletter.

Weekly only. No spam. Unsubscribe anytime.

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:

These are the most common failure points in a first private-registry deployment:

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.

From the lab

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

  • Synology DiskStation DS225+ - 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.

transmission signupstatus: open channel

article_topic // Self-Hosted Docker Registry Setup

Start building a smarter homelab.

Join readers learning Proxmox, networking, storage, backups, and self-hosting without breaking everything.

Enter your email address to receive the Homelab Addiction newsletter.

Beginner-friendly

No gatekeeping. Just clear, actionable guides.

1 useful email / week

Practical tips, real-world setups, and lessons learned.

Zero hype, practical only

What works, what breaks, and how to fix it.

Reply to any email with what you're building.
I read and reply to as many as I can.

— The Homelab Addiction Operator

support // the lab

Found this guide useful?

If it saved you time or a rebuild, you can support more practical homelab guides.

Support the lab