How to Set Up a Secure Private Docker Registry with HTTPS and a Web UI

Generate SSL Certificates
Let's generate a self-signed certificate:
mkdir certs
openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt
Create a Docker Compose File
Here’s a minimal docker-compose.yml
for a secure registry:
version: '3.8'
services:
registry:
image: registry:2
ports:
- "5000:5000"
restart: always
environment:
REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
REGISTRY_HTTP_TLS_KEY: /certs/domain.key
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
REGISTRY_STORAGE_DELETE_ENABLED: 'true'
volumes:
- ./docker-registry-data:/var/lib/registry
- ./certs:/certs
Start the registry:
docker compose up -d
Accessing Your Secure Registry
a. Trust the Certificate
- On your client machine, add
domain.crt
to your trusted certificates. - For self-signed certs, Docker clients need to trust the cert. Copy
domain.crt
to/etc/docker/certs.d/<your-registry-domain>:5000/ca.crt
on each client.
b. Test Access
curl -v --cacert certs/domain.crt https://<your-registry-domain>:5000/v2/
You should see an empty JSON {}
response, indicating the registry is running .
5. Pushing an Image to Your Private Registry
a. Tag Your Image
Suppose you have a local image called myapp:latest
:
docker tag myapp:latest <your-registry-domain>:5000/myapp:latest
b. Push the Image
docker push <your-registry-domain>:5000/myapp:latest
If you see errors about certificates, double-check that your client trusts the registry’s certificate .
c. Pull the Image (from another machine)
docker pull <your-registry-domain>:5000/myapp:latest
6. Installing a Web UI for Your Registry
A web UI makes it much easier to browse, manage, and delete images in your registry. One of the most popular options is Joxit/docker-registry-ui .
a. Add the UI to Your Compose File
Here’s a working example:
services:
registry-ui:
image: joxit/docker-registry-ui:main
restart: always
ports:
- 80:80
environment:
- SINGLE_REGISTRY=true
- REGISTRY_TITLE=Docker Registry UI
- DELETE_IMAGES=true
- SHOW_CONTENT_DIGEST=true
- NGINX_PROXY_PASS_URL=https://registry-server:5000
- SHOW_CATALOG_NB_TAGS=true
- CATALOG_MIN_BRANCHES=1
- CATALOG_MAX_BRANCHES=1
- TAGLIST_PAGE_SIZE=100
- REGISTRY_SECURED=false
- CATALOG_ELEMENTS_LIMIT=1000
container_name: registry-ui
registry-server:
image: registry:2.8.2
restart: always
environment:
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: '[http://registry-ui.example.com]'
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods: '[HEAD,GET,OPTIONS,DELETE]'
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials: '[true]'
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: '[Authorization,Accept,Cache-Control]'
REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers: '[Docker-Content-Digest]'
REGISTRY_STORAGE_DELETE_ENABLED: 'true'
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
REGISTRY_HTTP_TLS_KEY: /certs/domain.key
volumes:
- ./docker-registry-data:/var/lib/registry
- ./certs:/certs
container_name: registry-server
- The UI connects to the registry via the Docker network using the service name (
registry-server
). - CORS headers are set so the UI can communicate with the registry securely.
Start everything:
docker compose up -d
Visit http://<your-server-ip>/
in your browser to access the UI.
7. Lessons Learned: Troubleshooting and Key Takeaways
Setting up a secure Docker registry with a web UI is powerful, but it comes with some tricky parts:
- CORS headers must match exactly: The registry must send the correct
Access-Control-Allow-Origin
header matching your UI’s URL, or the browser will block requests. - Certificates must be trusted: Both the registry and any clients (including the UI) must trust your SSL certificate.
- Networking matters: Use Docker Compose service names for internal communication, and make sure ports are mapped correctly for external access.
- Environment variable formatting: Docker Compose environment variables must be strings, and for CORS headers, you often need to use JSON array syntax as a string (e.g.,
'["http://your-ui-domain"]'
). - Patience and careful debugging: Many issues come from small mismatches in URLs, ports, or certificate trust. Use
curl -v
and browser dev tools to inspect headers and responses.
Disclaimer: I wrote the raw version of this blogpost and beautified it via AI.