Self-Hosting a Docker Registry for Kamal Deployments

Self-Hosting a Docker Registry for Kamal Deployments

Self-Hosting a Docker Registry for Kamal Deployments

Moving from GitHub Container Registry to a private, self-hosted registry on your local network.


Why Self-Host?

GitHub Container Registry (GHCR) works well, but a self-hosted registry gives you:

  • Full control over your images and storage
  • Faster pushes/pulls on your local network — no round-trip to GitHub
  • No rate limits or token expiration headaches
  • Privacy — images never leave your network

The Stack

  • Registry (registry:latest) on port 5000 — Docker Registry v2 API
  • Registry UI (joxit/docker-registry-ui:latest) on port 8181 — Web interface for browsing and deleting images
  • Nginx Proxy Manager on ports 80/443 — TLS termination and reverse proxy

1. Docker Compose Setup

Create a docker-compose.yml on your registry host (e.g., 192.168.1.100):

services:
  registry:
    image: registry:latest
    ports:
      - "5000:5000"
    volumes:
      - registry-data:/var/lib/registry
    environment:
      - REGISTRY_STORAGE_DELETE_ENABLED=true
    restart: unless-stopped

  registry-ui:
    image: joxit/docker-registry-ui:latest
    ports:
      - "8181:80"
    environment:
      - REGISTRY_TITLE=Registry
      - NGINX_PROXY_PASS_URL=http://registry:5000
      - SINGLE_REGISTRY=true
      - DELETE_IMAGES=true
    restart: unless-stopped

volumes:
  registry-data:
docker compose up -d

At this point:
- Registry API is available at http://192.168.1.100:5000
- Registry UI is available at http://192.168.1.100:8181


2. Nginx Proxy Manager Configuration

Nginx Proxy Manager sits in front of both services, providing HTTPS via Let's Encrypt and clean domain names.

Proxy Host for my-registry.example.com (Registry API)

  • Domain Names: my-registry.example.com
  • Scheme: http
  • Forward Hostname / IP: 192.168.1.100
  • Forward Port: 5000
  • Websockets Support: Off
  • Block Common Exploits: On
  • SSL Certificate: Request new Let's Encrypt cert
  • Force SSL: On
  • HTTP/2 Support: On

Custom Nginx Configuration (Advanced tab) — required for large image pushes:

client_max_body_size 0;
chunked_transfer_encoding on;
proxy_set_header X-Forwarded-Proto $scheme;

Without client_max_body_size 0, Docker pushes will fail on layers larger than the default 1MB Nginx limit.

Proxy Host for my-registry-ui.example.com (Web UI)

  • Domain Names: my-registry-ui.example.com
  • Scheme: http
  • Forward Hostname / IP: 192.168.1.100
  • Forward Port: 8181
  • Block Common Exploits: On
  • SSL Certificate: Request new Let's Encrypt cert
  • Force SSL: On
  • HTTP/2 Support: On

3. DNS Records

In your DNS provider (or local DNS like Pi-hole/AdGuard), point both domains to the Nginx Proxy Manager host:

my-registry.example.com      → <NPM host IP>
my-registry-ui.example.com   → <NPM host IP>

4. Kamal Configuration

config/deploy.yml

The registry block changes from GHCR to your self-hosted registry:

# Before (GHCR)
registry:
  server: ghcr.io
  username: myuser
  password:
    - KAMAL_REGISTRY_PASSWORD

# After (self-hosted)
registry:
  server: my-registry.example.com
  username: myuser
  password:
    - KAMAL_REGISTRY_PASSWORD

The image field stays the same — Kamal prepends the registry server automatically:

image: myuser/myapp
# Resolves to: my-registry.example.com/myuser/myapp

.kamal/secrets

If the registry has no authentication, provide a placeholder value since Kamal requires the field:

SECRETS=$(kamal secrets fetch --adapter bitwarden \
  --account myuser@example.com \
  RAILS_MASTER_KEY POSTGRES_PASSWORD KAMAL_REGISTRY_PASSWORD)

RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS})
POSTGRES_PASSWORD=$(kamal secrets extract POSTGRES_PASSWORD ${SECRETS})
KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS})

5. Verify

# Test the registry API
curl https://my-registry.example.com/v2/_catalog

# Docker login (from build machine + deploy server)
docker login my-registry.example.com

# Deploy
bin/kamal deploy

Check the Registry UI at https://my-registry-ui.example.com to see your pushed images.


NPM Setup Steps

  1. Log into NPM at your Nginx Proxy Manager admin panel
  2. Go to HostsProxy HostsAdd Proxy Host
  3. Details tab — Fill in domain, scheme, forward IP, forward port
  4. SSL tab — Select "Request a new SSL Certificate", check Force SSL and HTTP/2
  5. Advanced tab — Paste the custom Nginx config (client_max_body_size 0; etc.) for the registry host
  6. Save — Repeat for the UI host

Related articles