Docker Registry is an open-source server-side application that lets you store and distribute Docker container images privately. Instead of pushing images to Docker Hub or other public registries, you run your own registry on infrastructure you control – keeping proprietary code and configurations off third-party servers. This guide walks through setting up a private Docker Registry on Rocky Linux 10 or AlmaLinux 10 with TLS encryption and basic authentication.
We cover installing Docker Engine, running the registry container, configuring TLS with both self-signed certificates and Let’s Encrypt, setting up htpasswd authentication, configuring storage backends, opening firewall ports, and pushing/pulling images from client machines. The Docker Distribution project (Registry v2) powers this setup.
Prerequisites
- A server running Rocky Linux 10 or AlmaLinux 10 with root or sudo access
- At least 2GB RAM and 20GB disk space (more for storing images)
- A domain name pointing to your server (required for Let’s Encrypt TLS – optional for self-signed)
- Docker Engine installed – follow our guide to install Docker on Rocky Linux 10 / AlmaLinux 10 if you haven’t already
- Port 5000/tcp (default registry port) or 443/tcp open in the firewall
Step 1: Install Docker Engine on Rocky Linux 10 / AlmaLinux 10
If Docker is not already installed, add the official Docker repository and install Docker CE. Switch to root first.
sudo -i
Install the yum-utils package and add the Docker CE repository.
dnf install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
Install Docker Engine, CLI, containerd, and the Compose plugin.
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Enable and start the Docker service.
systemctl enable --now docker
Verify Docker is running and check the installed version.
docker version
The output shows both the client and server versions, confirming Docker Engine is active.
Client: Docker Engine - Community
Version: 28.1.1
API version: 1.49
Server: Docker Engine - Community
Engine:
Version: 28.1.1
API version: 1.49 (minimum version 1.24)
Step 2: Create Directory Structure for the Registry
Create directories to store registry data, certificates, and authentication files. Keeping these organized makes backups and maintenance simpler.
mkdir -p /opt/registry/{data,certs,auth}
The /opt/registry/data directory holds all pushed images, certs stores TLS certificates, and auth contains the htpasswd file for authentication.
Step 3: Configure TLS Certificates for Docker Registry
Running a registry without TLS is only acceptable for local testing. For any production or network-accessible deployment, TLS is required. Docker clients refuse to communicate with insecure registries by default.
Option A: Self-Signed Certificates
Self-signed certificates work well for internal networks and lab environments. Generate a certificate and key pair valid for 5 years.
openssl req -newkey rsa:4096 -nodes -sha256 \
-keyout /opt/registry/certs/registry.key \
-x509 -days 1825 \
-out /opt/registry/certs/registry.crt \
-subj "/CN=registry.example.com" \
-addext "subjectAltName=DNS:registry.example.com,IP:10.0.1.10"
Replace registry.example.com with your actual hostname or IP. The subjectAltName extension is required – Docker clients reject certificates without it.
Verify the certificate was created and check its details.
openssl x509 -in /opt/registry/certs/registry.crt -noout -subject -dates
The output confirms the certificate subject and validity period.
subject=CN = registry.example.com
notBefore=Mar 21 10:00:00 2026 GMT
notAfter=Mar 20 10:00:00 2031 GMT
Option B: Let’s Encrypt Certificates
For public-facing registries, Let’s Encrypt provides free, trusted TLS certificates. You need a domain name pointing to your server. Install Certbot first.
dnf install -y epel-release
dnf install -y certbot
Request a certificate using the standalone method. Stop any service using port 80 before running this.
certbot certonly --standalone -d registry.example.com --agree-tos -m [email protected]
Copy the generated certificates to the registry directory. For details on certificate management, see our guide on generating Let’s Encrypt SSL certificates on Linux.
cp /etc/letsencrypt/live/registry.example.com/fullchain.pem /opt/registry/certs/registry.crt
cp /etc/letsencrypt/live/registry.example.com/privkey.pem /opt/registry/certs/registry.key
Set up automatic renewal with a post-hook that copies the renewed certificates and restarts the registry container.
cat > /etc/letsencrypt/renewal-hooks/deploy/registry.sh << 'SCRIPT'
#!/bin/bash
cp /etc/letsencrypt/live/registry.example.com/fullchain.pem /opt/registry/certs/registry.crt
cp /etc/letsencrypt/live/registry.example.com/privkey.pem /opt/registry/certs/registry.key
docker restart registry
SCRIPT
chmod +x /etc/letsencrypt/renewal-hooks/deploy/registry.sh
Step 4: Set Up Authentication with htpasswd
Without authentication, anyone who can reach your registry can push and pull images. Use htpasswd to create a password file with bcrypt hashing - the only algorithm the registry supports.
Install the httpd-tools package which provides the htpasswd utility.
dnf install -y httpd-tools
Create the first user. The -Bc flags specify bcrypt hashing and create a new file.
htpasswd -Bc /opt/registry/auth/htpasswd admin
You will be prompted to enter and confirm the password. To add more users later, use -B without -c (the -c flag overwrites the file).
htpasswd -B /opt/registry/auth/htpasswd developer
Step 5: Run the Docker Registry Container
Start the registry container with TLS and authentication enabled. This maps port 5000 on the host to the registry container and mounts the certificate, authentication, and data directories.
docker run -d \
--name registry \
--restart=always \
-p 5000:5000 \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/certs:/certs \
-v /opt/registry/auth:/auth \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/registry.key \
-e REGISTRY_AUTH=htpasswd \
-e REGISTRY_AUTH_HTPASSWD_REALM="Docker Registry" \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
registry:2
Check that the container is running and healthy.
docker ps --filter name=registry
The output should show the registry container with status "Up" and port 5000 mapped.
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
a1b2c3d4e5f6 registry:2 "/entrypoint.sh /etc…" Up 2 seconds 0.0.0.0:5000->5000/tcp registry
Test the registry endpoint with curl. Replace the hostname with your server's address.
curl -u admin -k https://registry.example.com:5000/v2/_catalog
Enter your password when prompted. An empty registry returns this response.
{"repositories":[]}
Step 6: Configure Storage Backend
The default configuration stores images on the local filesystem at /var/lib/registry inside the container (mapped to /opt/registry/data on the host). For production, you may want to configure storage limits or use an external backend. Refer to the Docker Distribution configuration reference for all storage options.
Create a custom configuration file for advanced storage settings.
vi /opt/registry/config.yml
Add the following configuration. This sets the filesystem storage with a delete option enabled and configures garbage collection.
version: 0.1
log:
fields:
service: registry
storage:
filesystem:
rootdirectory: /var/lib/registry
maxthreads: 100
delete:
enabled: true # Allow image deletion via API
cache:
blobdescriptor: inmemory
http:
addr: :5000
tls:
certificate: /certs/registry.crt
key: /certs/registry.key
auth:
htpasswd:
realm: "Docker Registry"
path: /auth/htpasswd
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
Stop and remove the existing registry container, then restart with the custom config file.
docker stop registry && docker rm registry
Start the registry with the custom configuration mounted.
docker run -d \
--name registry \
--restart=always \
-p 5000:5000 \
-v /opt/registry/config.yml:/etc/docker/registry/config.yml \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/certs:/certs \
-v /opt/registry/auth:/auth \
registry:2
Step 7: Open Firewall Ports
Rocky Linux 10 and AlmaLinux 10 use firewalld by default. Open port 5000/tcp so Docker clients on other machines can reach the registry. For a complete firewalld configuration guide on Rocky Linux 10, check our dedicated article.
firewall-cmd --permanent --add-port=5000/tcp
firewall-cmd --reload
Verify the port is open.
firewall-cmd --list-ports
You should see port 5000/tcp in the listed ports.
5000/tcp
If you run the registry on port 443 instead, open that port.
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
Step 8: Configure Docker Clients to Use the Private Registry
Every machine that needs to push or pull images from your private registry must be configured to trust its TLS certificate and authenticate.
For Self-Signed Certificates
Copy the registry's CA certificate to each Docker client. Docker looks for trusted certificates in /etc/docker/certs.d/.
mkdir -p /etc/docker/certs.d/registry.example.com:5000
Copy the certificate from the registry server to the client. Run this on the client machine.
scp [email protected]:/opt/registry/certs/registry.crt \
/etc/docker/certs.d/registry.example.com:5000/ca.crt
No Docker restart is needed - Docker picks up certificates from this directory automatically.
For Let's Encrypt Certificates
Let's Encrypt certificates are issued by a publicly trusted CA, so Docker clients trust them automatically. No extra configuration is needed on client machines.
Log In to the Registry
On each client machine, log in to the private registry using the credentials you created with htpasswd.
docker login registry.example.com:5000
Enter the username and password when prompted. A successful login shows this message.
Login Succeeded
Docker stores the credentials in ~/.docker/config.json. Subsequent push and pull operations use these saved credentials.
Step 9: Push and Pull Images from the Private Registry
With the registry running and clients configured, test the full workflow by pushing an image and pulling it back.
Pull a small test image from Docker Hub.
docker pull alpine:latest
Tag the image with your private registry address. The tag format is registry-address/repository:tag.
docker tag alpine:latest registry.example.com:5000/alpine:latest
Push the tagged image to your private registry.
docker push registry.example.com:5000/alpine:latest
The push output shows each layer being uploaded to the registry.
The push refers to repository [registry.example.com:5000/alpine]
e2eb06d8af82: Pushed
latest: digest: sha256:abc123... size: 528
Verify the image is stored in the registry by querying the catalog API.
curl -u admin -k https://registry.example.com:5000/v2/_catalog
The response now shows the alpine repository.
{"repositories":["alpine"]}
To pull the image from another client, remove the local copy first and then pull from the private registry.
docker rmi registry.example.com:5000/alpine:latest
docker pull registry.example.com:5000/alpine:latest
The image downloads from your private registry instead of Docker Hub.
Step 10: List Tags and Manage Images
View all tags for a specific repository using the registry API.
curl -u admin -k https://registry.example.com:5000/v2/alpine/tags/list
The response lists all available tags for the alpine image.
{"name":"alpine","tags":["latest"]}
To delete an image, first get the digest, then send a DELETE request. This requires delete.enabled: true in the registry configuration.
digest=$(curl -s -u admin -k -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://registry.example.com:5000/v2/alpine/manifests/latest \
-I | grep Docker-Content-Digest | awk '{print $2}' | tr -d '\r')
curl -u admin -k -X DELETE \
https://registry.example.com:5000/v2/alpine/manifests/$digest
After deleting image manifests, run garbage collection to reclaim disk space. Execute this inside the registry container.
docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml
Step 11: Run Registry on Port 443 (Optional)
Running the registry on port 443 lets clients connect without specifying a port number in image tags. Stop the existing registry and restart it with port 443 mapped.
docker stop registry && docker rm registry
Start the registry on port 443. The container still listens on 5000 internally, but the host maps 443 to it.
docker run -d \
--name registry \
--restart=always \
-p 443:5000 \
-v /opt/registry/config.yml:/etc/docker/registry/config.yml \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/certs:/certs \
-v /opt/registry/auth:/auth \
registry:2
With port 443, image tags become cleaner. Instead of registry.example.com:5000/alpine:latest, use registry.example.com/alpine:latest.
Step 12: Configure Docker Registry as a Systemd Service
The --restart=always flag handles container restarts, but you can also manage the registry through systemd for better integration with system startup and logging. If you need to run Docker containers as systemd services, create a unit file.
vi /etc/systemd/system/docker-registry.service
Add the following systemd unit configuration.
[Unit]
Description=Docker Registry
After=docker.service
Requires=docker.service
[Service]
Restart=always
ExecStartPre=-/usr/bin/docker stop registry
ExecStartPre=-/usr/bin/docker rm registry
ExecStart=/usr/bin/docker run --name registry \
-p 5000:5000 \
-v /opt/registry/config.yml:/etc/docker/registry/config.yml \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/certs:/certs \
-v /opt/registry/auth:/auth \
registry:2
ExecStop=/usr/bin/docker stop registry
[Install]
WantedBy=multi-user.target
Reload systemd and enable the service.
systemctl daemon-reload
systemctl enable --now docker-registry
Verify the service is running.
systemctl status docker-registry
The output should show the service as active (running).
● docker-registry.service - Docker Registry
Loaded: loaded (/etc/systemd/system/docker-registry.service; enabled; preset: disabled)
Active: active (running) since Fri 2026-03-21 10:30:00 UTC
Main PID: 12345 (docker)
Tasks: 8 (limit: 23456)
Memory: 45.2M
CPU: 1.234s
Docker Registry Port Reference
| Port | Protocol | Purpose |
|---|---|---|
| 5000 | TCP | Default Docker Registry API port |
| 443 | TCP | HTTPS - standard port for production registries |
Conclusion
You now have a private Docker Registry running on Rocky Linux 10 or AlmaLinux 10 with TLS encryption and htpasswd authentication. Images are stored locally on the server and accessible to any Docker client configured with the registry's certificate and credentials.
For production hardening, consider placing the registry behind an Nginx reverse proxy with rate limiting, setting up automated backups of /opt/registry/data, monitoring disk usage with alerts, and restricting network access to trusted client IPs only. You can also explore setting up a Docker registry on Ubuntu if you manage mixed environments.