Set up a Private Docker Registry with htpasswd Authentication and Pull‑Through Mirror on a VPS: step‑by‑step
Docker & Containers · By Kevin Marcondes · Published on · Updated on · 7 min read
In this tutorial you’ll deploy a Docker Registry on a VPS using htpasswd authentication, enable Docker Hub pull‑through caching (mirror), and protect it with HTTPS via a reverse proxy. At the end, your Docker clients will pull public images via the mirror and push private images securely.
1. Prerequisites and architecture we’ll build
You need an up-to-date Linux VPS, a domain (e.g., registry.example.com) pointing to the server, and ports 80/443 open. You will need root or sudo access.
We’ll use Docker + Compose to run: (1) a private registry with htpasswd authentication; (2) a pull‑through cache for Docker Hub; and (3) a reverse proxy with HTTPS.
If you haven’t installed them yet, follow the guide How to install Docker and Docker Compose on Linux. Harden the server using the VPS Linux security checklist and open firewall ports; see How to set up a firewall on Ubuntu with UFW.
- VPS with CPU/RAM/disk sized for your image volume.
- DNS A/AAAA for registry.example.com pointing to the VPS.
- Docker and Docker Compose installed.
- Ports 80/443 and 5000 allowed in the firewall.
2. Create directories and the htpasswd file
Create the data structure and generate the htpasswd. We’ll use the httpd image only to create the password file. Replace the username and password with strong values and store them securely.
sudo mkdir -p /opt/registry/{data,auth}
# Gere um usuário e senha para acessar o registry
# Substitua registryuser e a senha por valores fortes
docker run --rm --entrypoint htpasswd httpd:2 -Bbn registryuser 'S3nh@F0rte' \
> /opt/registry/auth/htpasswd
# Revise permissões conforme sua política de segurança
sudo chmod 640 /opt/registry/auth/htpasswd
sudo chown root:root /opt/registry/auth/htpasswd
3. Start the Docker Registry with authentication (port 5000)
Start the registry with htpasswd authentication. At this stage we run without TLS because HTTPS will be handled by the reverse proxy later. Restrict firewall access until HTTPS is configured.
# docker-compose.yml
version: "3.9"
services:
registry:
image: registry:2
container_name: registry
restart: unless-stopped
ports:
- "5000:5000"
environment:
- REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/var/lib/registry
- REGISTRY_AUTH=htpasswd
- REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
- REGISTRY_STORAGE_DELETE_ENABLED=true
volumes:
- /opt/registry/data:/var/lib/registry
- /opt/registry/auth:/auth
# Suba o serviço (na mesma pasta do docker-compose.yml)
# Compose v2:
docker compose up -d registry
4. Enable Docker Hub pull‑through cache (mirror)
To reduce latency and save bandwidth, enable mirroring. The registry will cache public Docker Hub images as clients request them. This is not a full mirror; only requested images are cached.
- Stores layers on local disk, speeding up repeated pulls.
- Respects Docker Hub rate limits; you can optionally set Hub credentials.
# Edite docker-compose.yml e adicione a variável de proxy
services:
registry:
environment:
- REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io
# Opcional: para limites do Hub, defina usuário/senha do Hub
# - REGISTRY_PROXY_USERNAME=seu_usuario
# - REGISTRY_PROXY_PASSWORD=sua_senha
# Recrie o serviço
docker compose up -d registry
# Verifique logs
docker logs -f registry
5. HTTPS with a reverse proxy (Caddy) and your domain
Place the registry behind Caddy to get automatic HTTPS. Make sure DNS resolves to the VPS and ports 80/443 are open, since ACME needs to validate the domain.
If you prefer Traefik or Nginx, compare options in Traefik vs Caddy: HTTPS Routing in Docker and choose what fits your stack.
- Automatic TLS and transparent renewal.
- Reverse proxy that’s simple to configure with Caddy.
# Adicione o Caddy ao docker-compose.yml
services:
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- registry
volumes:
caddy_data:
caddy_config:
---
# Crie o arquivo Caddyfile na mesma pasta
# Substitua registry.example.com pelo seu domínio
registry.example.com {
encode gzip
reverse_proxy registry:5000
}
# Suba/recrie tudo
docker compose up -d
6. Configure Docker clients to use the mirror and registry
On client machines (dev workstations, CI servers), configure the mirror and restart the Docker daemon. Restarting Docker may interrupt running containers. Then log in to perform pushes to your domain.
- Mirror speeds up pulls for public Docker Hub images.
- Push/pull of private images uses your domain with login.
# Linux (systemd): configure o mirror e reinicie o Docker
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{
"registry-mirrors": ["https://registry.example.com"],
"insecure-registries": [],
"log-driver": "json-file",
"log-opts": {"max-size": "10m", "max-file": "3"}
}
JSON
sudo systemctl restart docker
# Verifique
docker info | grep -A2 'Registry Mirrors'
7. Publish and consume private images; test the mirror
After configuring the client, authenticate and push an image to your registry. Also verify the mirror by pulling a common public image after setting the registry mirror.
# Faça login no seu registry
docker login registry.example.com
# Marque e envie sua imagem
# Exemplo: container local "myapp:1.0"
docker tag myapp:1.0 registry.example.com/minhaorg/myapp:1.0
docker push registry.example.com/minhaorg/myapp:1.0
# Teste pull privado
docker pull registry.example.com/minhaorg/myapp:1.0
# Teste o mirror (imagem pública do Hub)
docker pull nginx:alpine # deve ser servido via mirror após primeiro pull
8. Verification and common troubleshooting
Confirm access and inspect logs. For HTTPS problems, check DNS and firewall. To save space, consider tag cleanup and the registry garbage collector. Running garbage‑collect removes orphaned layers; back up data first.
- Test the registry API: catalog and tags.
- Check logs of both the registry and the proxy.
- Certificate errors: verify domain and open ports.
- Docker Hub rate limits: consider adding Hub credentials to the proxy.
# Testes rápidos (ajuste domínio/credenciais)
curl -u registryuser:S3nh@F0rte -fsSL https://registry.example.com/v2/_catalog | jq .
# Listar tags de um repositório
curl -u registryuser:S3nh@F0rte -fsSL https://registry.example.com/v2/minhaorg/myapp/tags/list | jq .
# Logs
docker logs --tail=100 registry
# (Opcional) Garbage collect — pare o serviço antes
# docker compose stop registry
# docker run --rm -v /opt/registry/data:/var/lib/registry \
# -v /opt/registry/auth:/auth registry:2 \
# garbage-collect /etc/docker/registry/config.yml
Frequently asked questions
What’s the difference between a private registry and a mirror (pull‑through cache)?
A private registry stores your images under your domain and control. A mirror is a cache for public Docker Hub images: it downloads on demand, stores layers locally, and speeds up future pulls. You can run both behaviors in the same service with authentication.
Do I need authentication to use the mirror?
For public image pulls, the mirror can be exposed without authentication if you choose. However, it’s recommended to require authentication for all access. For pushing private images, authentication is mandatory via docker login.
Where are cached layers stored?
Layers are stored in the registry’s configured directory, mounted at /var/lib/registry. Ensure enough disk space and use fast storage. Back up periodically if those images are critical.
How do I free disk space on the registry?
Remove unused tags and run the registry garbage‑collect with the service stopped. Garbage collection removes orphaned layers; back up before running it because it’s destructive to unreferenced blobs. Schedule maintenance to avoid downtime.
Can I use my own TLS certificate instead of ACME?
Yes. In Caddy or your proxy, disable automatic management and point to your certificate and key files. Provide the full chain to clients. If you use an internal CA, add the root certificate to client trust stores.
Does the mirror speed up images from other private registries?
No. The pull‑through cache described here targets the public Docker Hub. Pulls from third‑party private registries are not cached by this mirror. For those, authenticate and pull directly from the origin registry.
Need help adapting this setup to your environment?
If you want a review of topology, access policies, TLS automation, or CI/CD integration, we can discuss recommended approaches and common risks.
Discuss your scenarioSee also
- Tutorial: Implement Canary releases with Traefik in Docker Compose using headers and weighted routing for gradual PHP deployments
- Tutorial: Configure HAProxy with Let's Encrypt and health checks to load balance traffic between Docker containers on a VPS (step-by-step)
- Hands‑On Tutorial: Implement mTLS Between Nginx and Microservices with smallstep CA in Docker Compose
- Tutorial: configure cgroup v2 and systemd ResourceControl to limit CPU, memory and I/O for Docker containers on Debian