Set Up a Private Docker Registry
Run your own container registry with authentication and TLS – and when a hosted registry is the better choice.
Show setupSometimes you want your images on your own infrastructure. The official registry image (based on CNCF Distribution) makes that straightforward. The important part is to never run it without authentication and TLS.
1. Create a user
The registry supports htpasswd files with bcrypt hashes. The registry image no longer ships the htpasswd tool, so use the Apache image to create the file:
mkdir -p auth certs data
docker run --rm --entrypoint htpasswd httpd:2 -Bbn myuser 'a-strong-password' > auth/htpasswd
2. Add certificates
Put your certificate and key into certs/ (e.g. from Let's Encrypt) as registry.crt and registry.key. If you terminate TLS in a reverse proxy instead, see step 5.
3. Start the registry
compose.yaml
services:
registry:
image: registry:2
restart: unless-stopped
ports:
- "443:5000"
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_HTTP_TLS_CERTIFICATE: /certs/registry.crt
REGISTRY_HTTP_TLS_KEY: /certs/registry.key
REGISTRY_STORAGE_DELETE_ENABLED: "true"
volumes:
- ./auth:/auth:ro
- ./certs:/certs:ro
- ./data:/var/lib/registry
docker compose up -d
Check Docker Hub for the current major version of the registry image before you pin it.
4. Push an image
docker login registry.example.com
docker tag myapp:latest registry.example.com/myapp:latest
docker push registry.example.com/myapp:latest
5. Behind nginx
If nginx terminates TLS, remove the two REGISTRY_HTTP_TLS_* variables, publish the registry only locally (e.g. 127.0.0.1:5000:5000) and add a server block like this:
server {
listen 443 ssl;
server_name registry.example.com;
ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
# Image layers can be large – disable the upload size limit
client_max_body_size 0;
chunked_transfer_encoding on;
location /v2/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900;
}
}
6. Free up storage
Deleting a tag doesn't free disk space by itself. Run the garbage collector afterwards:
docker compose exec registry bin/registry garbage-collect /etc/docker/registry/config.yml
Do you need your own registry?
Running a registry means maintaining it: updates, backups, certificates. Often a managed option is simpler:
- GitHub Container Registry, GitLab Container Registry – close to your code and CI.
- Amazon ECR, Google Artifact Registry, Azure Container Registry – if you're on that cloud anyway.
- Harbor – if you want to self-host with a UI, vulnerability scanning and fine-grained permissions.