A private registry lets your servers and CI pipelines push and pull images without relying on a third-party service or its pull limits. The official registry image from the Docker Distribution project does the job in one container. This guide runs it behind an HTTPS reverse proxy with password protection, then shows how to push, pull and clean up.
Before you start
- A Linux VPS with Docker Engine and the Compose plugin. See Install Docker and Docker Compose.
- A domain or subdomain, such as
registry.example.com, with an A record pointing at the VPS. - Ports 80 and 443 open in your firewall for the reverse proxy.
- Enough disk for your images. Plan for several times the size of the images you push, because every tag you keep stores its own layers.
Note: This guide uses the registry:2 tag, which is the long-established major version. A newer major version of the image also exists. Check the image's page on Docker Hub and the Distribution project's release notes before you choose, because configuration paths can differ between major versions.
Create the folders and a password file
sudo mkdir -p /opt/registry/{data,auth}
cd /opt/registryBasic auth needs an htpasswd file with bcrypt hashes. Generate it with the httpd image so you do not need to install Apache tools:
docker run --rm --entrypoint htpasswd httpd:2 -Bbn deploy 'a-long-random-password' | sudo tee auth/htpasswdReplace deploy and the password with your own. Add more users by running the command again with tee -a.
Write the Compose file
Create /opt/registry/compose.yaml. This example uses Caddy as the reverse proxy because it obtains a Let's Encrypt certificate automatically:
services:
registry:
image: registry:2
restart: unless-stopped
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_STORAGE_DELETE_ENABLED: "true"
volumes:
- ./data:/var/lib/registry
- ./auth:/auth:ro
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:The registry has no ports: entry. Only Caddy can reach it, over the Compose network.
Create /opt/registry/Caddyfile:
registry.example.com {
request_body {
max_size 5GB
}
reverse_proxy registry:5000
}The body size limit allows large layers. Caddy passes the authentication headers through unchanged, so the registry handles logins itself.
If you already run Nginx as a reverse proxy, point it at the registry instead. Set client_max_body_size 0; so large layer uploads are not rejected, and pass the Host and X-Forwarded-Proto headers.
Start the registry
cd /opt/registry
docker compose up -d
docker compose logs -f caddyWait until Caddy logs that it obtained a certificate, then press Ctrl + C.
Push and pull images
Log in from any machine with Docker:
docker login registry.example.comTag an image with the registry's address and push it:
docker tag myapp:1.0 registry.example.com/myapp:1.0
docker push registry.example.com/myapp:1.0Pull it on another server after logging in there:
docker pull registry.example.com/myapp:1.0In Compose files, use the full name as the image: value.
Tip: Create a separate user per server or CI system in the htpasswd file. You can then remove one machine's access without changing every other password.
Delete images and run garbage collection
Deleting a tag through the registry API only removes the reference. The layers stay on disk until you run garbage collection.
List repositories and tags:
curl -u deploy https://registry.example.com/v2/_catalog
curl -u deploy https://registry.example.com/v2/myapp/tags/listTo delete a tag, find its manifest digest and send a DELETE request:
curl -u deploy -sI \
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
https://registry.example.com/v2/myapp/manifests/1.0 | grep -i docker-content-digest
curl -u deploy -X DELETE https://registry.example.com/v2/myapp/manifests/sha256:DIGESTImages built with BuildKit may use the OCI manifest format. If the first command returns no digest, repeat it with Accept: application/vnd.oci.image.index.v1+json.
Then reclaim the space. Do a dry run first:
Warning: Do not push images while garbage collection runs. Stop the registry, or make sure no CI job is pushing, before you run it.
docker compose stop registry
docker compose run --rm registry garbage-collect --dry-run /etc/docker/registry/config.yml
docker compose run --rm registry garbage-collect --delete-untagged /etc/docker/registry/config.yml
docker compose start registrySize and back up the storage
- Check usage with
sudo du -sh /opt/registry/data. - Keep only the tags you might roll back to. A CI job that pushes a tag per commit can fill a disk quickly.
- Back up
/opt/registry/dataandauthwith your normal file backups. For large registries, the registry can also store data in S3-compatible object storage; see the Distribution project's storage driver documentation.
Check it works
The API base answers with 401 without credentials and 200 with them:
curl -s -o /dev/null -w '%{http_code}\n' https://registry.example.com/v2/
curl -s -o /dev/null -w '%{http_code}\n' -u deploy https://registry.example.com/v2/Then push a small test image and pull it back from a second machine.
Troubleshooting
413 Request Entity Too Largewhile pushing: the reverse proxy limits body size. Raisemax_sizein Caddy or setclient_max_body_size 0;in Nginx.no basic auth credentialsorunauthorized: rundocker login registry.example.comagain on that machine, and check thehtpasswdfile was created with-B(bcrypt).http: server gave HTTP response to HTTPS client: you are reaching the registry directly on port 5000. Always use the HTTPS domain through the proxy.- Disk did not shrink after deleting tags: garbage collection has not run, or
REGISTRY_STORAGE_DELETE_ENABLEDwas not set when you deleted.
Next steps
- Prefer a hosted registry? See Push images to Docker Hub or GitHub Container Registry.
- Build and push from CI: GitHub Actions: build and deploy Docker images.
- Learn more about the proxy used here: Caddy reverse proxy for Docker apps.
- Get disk space for your registry on a Linux VPS.
