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
- Log into NPM at your Nginx Proxy Manager admin panel
- Go to Hosts → Proxy Hosts → Add Proxy Host
- Details tab — Fill in domain, scheme, forward IP, forward port
- SSL tab — Select "Request a new SSL Certificate", check Force SSL and HTTP/2
- Advanced tab — Paste the custom Nginx config (
client_max_body_size 0;etc.) for the registry host - Save — Repeat for the UI host