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 setup

Sometimes 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.