AlmaLinux

Setup Private Docker Registry on Rocky Linux 10 / AlmaLinux 10

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

PortProtocolPurpose
5000TCPDefault Docker Registry API port
443TCPHTTPS - 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.

Related Articles

Cloud Install OpenStack Dalmatian on Rocky Linux 10 with Packstack Containers Run Kuma Self-hosted Uptime Robot in Docker Container AlmaLinux Fix ifup Command Not Found on RHEL 10 / Rocky Linux 10 / AlmaLinux 10 Containers Configure Active Directory (AD) Authentication for Harbor Registry

Press ESC to close