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 [email protected] \
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
Credits
- Photo by Tom Fisk via Pexels
Comments
Leave a comment
Your comment will appear after moderation.
Your name and email are only used to identify your comment. Your email is never shown publicly. Privacy policy.
Related articles
Homelab
AdGuardHome on Asuswrt-Merlin
Home should always be a safe place for navigating the internet, especially for children. As they often use it for listening to music or…
Read the article →
Github-actions
Run rails tests with github actions matrix strategy
You can easily speedup tests runs by using matrix strategy with github actions. Create a script called for example ci_tests which generates…
Read the article →
Docker
Running ipinfo.tw on dokku
To obtain my external IP, I chose to use ipinfo.tw. Initially, I fetched my IP via https://ipinfo.tw, but I decided to host it on my own…
Read the article →