Deployment guide

Docker Registry 3.1

Overview

Docker Registry is powered by the CNCF Distribution project, the reference open-source container image registry that Docker Hub and most private registries are built on. It is a stateless, highly scalable server-side application that stores and distributes OCI and Docker container images.

This SecureAnyCloud image runs Distribution 3.1 on Ubuntu 24.04 on Azure. It installs the official pinned Distribution v3.1.1 release binary (a single static Go binary, sha256 verified) to /opt/registry/registry and runs it directly under systemd. Docker is not required to run the registry, because the registry is a self-contained server.

The registry binary binds to loopback only (127.0.0.1:5000) and does no authentication of its own. nginx is the sole network-facing surface. It terminates TLS on port 443 and enforces HTTP Basic authentication against a bcrypt htpasswd file.

Secure by design, with no anonymous access. A registry without authentication is an open push and pull surface for anyone who can reach it. On the first boot of every VM, this image generates a per-instance admin credential (a random openssl rand -base64 24 secret) and writes it into the nginx htpasswd file. It also regenerates a fresh self-signed TLS certificate for that VM, with Subject Alternative Names that include the VM public IP and hostname.

Anonymous requests to the registry API return HTTP 401 with a WWW-Authenticate: Basic challenge, and only the per-instance credential is accepted. The credential is written to /root/registry-credentials.txt (mode 0600, root only).

What you get:

  • Docker Registry powered by CNCF Distribution v3.1.1 (official release binary, sha256 pinned) at /opt/registry/registry, run under systemd as the unprivileged registry system user
  • The registry API bound to loopback only (127.0.0.1:5000), never directly exposed to the network
  • nginx terminating TLS on :443 and enforcing htpasswd Basic auth on the registry API, with client_max_body_size 0 and chunked_transfer_encoding on for large layer pushes
  • Port :80 returning a 301 redirect to HTTPS for every path except an unauthenticated /healthz endpoint (HTTP 200) for load balancer probes
  • A per-instance admin credential generated on first boot and stored in /root/registry-credentials.txt (0600), with no anonymous access and no shared default password
  • A self-signed TLS certificate regenerated per VM on first boot (SAN includes the VM public IP and hostname), which you can replace with your own CA-signed certificate for production
  • Image blobs stored on a dedicated 20 GiB Azure data disk mounted at /var/lib/registry, so your pushed images survive independently of the OS disk and the volume can be resized independently
  • Ubuntu 24.04 LTS base with the latest security patches applied at build time
  • Azure Linux Agent for cloud integration and SSH key injection
  • 24/7 SecureAnyCloud support with a guaranteed 24-hour response SLA

Prerequisites

You need:

  • An active Azure subscription, an SSH public key, and a VNet with a subnet in the target region
  • A subscription to the Docker Registry (Distribution) listing on the Azure Marketplace
  • A machine with the Docker CLI installed to push and pull images (any recent Docker Engine or Docker Desktop)

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) suits a team registry and light workloads.

For many concurrent pulls or large images, use Standard_D4s_v5 (4 vCPU, 16 GB RAM) or larger, with a larger data disk.

1

Deploy from the Azure Portal

Search the Azure Marketplace for Docker Registry, select the SecureAnyCloud publisher, and click Create.

Configure the network security group (NSG) to allow these ports from your client networks:

  • TCP 22 for administration
  • TCP 443 for the registry API over HTTPS
  • TCP 80 for the HTTP-to-HTTPS redirect and health probe
2

Deploy from the Azure CLI

RG="registry-prod"; LOCATION="eastus"; VM_NAME="registry-01" IMAGE_URN="sac1647283583153:docker-registry-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION" az network vnet create -g "$RG" --name registry-vnet --address-prefix 10.100.0.0/16 --subnet-name registry-subnet --subnet-prefix 10.100.1.0/24 az network nsg create -g "$RG" --name registry-nsg az network nsg rule create -g "$RG" --nsg-name registry-nsg --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp az network nsg rule create -g "$RG" --nsg-name registry-nsg --name allow-https --priority 110 \
  --destination-port-ranges 443 --access Allow --protocol Tcp az network nsg rule create -g "$RG" --nsg-name registry-nsg --name allow-http --priority 120 \
  --destination-port-ranges 80 --access Allow --protocol Tcp
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:docker-registry-ubuntu-24-04:default:latest
az vm create -g "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username azureuser --ssh-key-values "$SSH_KEY" \
  --vnet-name registry-vnet --subnet registry-subnet --nsg registry-nsg --public-ip-sku Standard
3

Connect via SSH and retrieve your registry credential

Each VM generates its own admin credential on first boot and stores it in a root-only file. Connect over SSH and read it:

ssh azureuser@<public-ip>
sudo cat /root/registry-credentials.txt

The file contains the registry URL, the username (admin), the per-instance password and quick-start commands. Store the password somewhere safe, because it is unique to this VM.

4

Verify the registry and nginx services

Confirm that both services are active and that the registry is bound to loopback only. In the listener output, 127.0.0.1:5000 is the registry and is reachable only locally. nginx listens publicly on :443 and :80:

systemctl status registry nginx --no-pager ss -tln | grep -E ':5000|:443|:80 '
5

Check the health endpoint

nginx serves the /healthz endpoint without authentication, which makes it suitable for load balancer and Azure health probes. It returns HTTP 200:

curl -k -o /dev/null -w 'healthz: HTTP %{http_code}\n' https://127.0.0.1/healthz
6

Query the registry API and confirm anonymous access is rejected

Use the per-instance credential to list repositories through the registry API. Then confirm that an anonymous request, with no credential, is rejected with HTTP 401. The self-signed certificate lives at /etc/nginx/tls/registry.crt, so pass it with --cacert:

PASS=$(sudo grep '^registry.admin.pass=' /root/registry-credentials.txt | cut -d= -f2-)
echo "# Authenticated catalog request:"
sudo curl -s -u "admin:$PASS" --cacert /etc/nginx/tls/registry.crt https://127.0.0.1/v2/_catalog; echo echo "# Anonymous request (no credential) must be rejected:" curl -s -o /dev/null -w 'anonymous /v2/ -> HTTP %{http_code}\n' --cacert /etc/nginx/tls/registry.crt https://127.0.0.1/v2/

The authenticated request returns the repository list as JSON, and the anonymous request returns HTTP 401. There is no anonymous access to this registry.

Issue the certificate for your public address

On first boot, the VM writes the address Azure reports into the certificate, the credentials file and the login banner. With a Standard SKU public IP (the Azure default, and the --public-ip-sku Standard in Step 2), the current image records the VM's private address (10.0.0.x). Docker and curl --cacert then refuse the certificate at the public IP. Check which address was recorded:

sudo grep registry.url /root/registry-credentials.txt

If it shows a 10. address, issue a new certificate for the public IP on the VM. Azure's instance metadata service reports the public IP to the VM itself, and nothing leaves the VM:

PUBLIC_IP=$(curl -s -H Metadata:true 'http://169.254.169.254/metadata/loadbalancer?api-version=2020-10-01' | grep -o '"frontendIpAddress":"[^"]*"' | head -1 | cut -d'"' -f4)
PRIV=$(hostname -I | awk '{print $1}'); echo "public $PUBLIC_IP, private $PRIV"
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
  -keyout /etc/nginx/tls/registry.key -out /etc/nginx/tls/registry.crt \
  -subj "/C=GB/O=sac/CN=$PUBLIC_IP" \
  -addext "subjectAltName=IP:127.0.0.1,DNS:localhost,DNS:$(hostname -s),IP:$PRIV,IP:$PUBLIC_IP" sudo chmod 0600 /etc/nginx/tls/registry.key
sudo sed -i "s/\b$PRIV\b/$PUBLIC_IP/g" /root/registry-credentials.txt /etc/update-motd.d/90-docker-registry
sudo systemctl restart nginx

For a DNS name, set PUBLIC_IP to the name and write DNS:$PUBLIC_IP in place of IP:$PUBLIC_IP. Your credential does not change. You copy the new certificate in the next step.

7

Push and pull images with the Docker CLI

From any machine with Docker installed, first trust the registry's self-signed certificate, or replace it with your own CA-signed certificate (see Step 9). Then log in with your per-instance credential and push an image. Replace <public-ip> with your VM's public IP or DNS name.

Copy the certificate from the VM and install it as a trusted CA for this registry:

scp azureuser@<public-ip>:/etc/nginx/tls/registry.crt registry.crt
sudo mkdir -p /etc/docker/certs.d/<public-ip>
sudo cp registry.crt /etc/docker/certs.d/<public-ip>/ca.crt

Log in, tag an image for your registry, push it, then pull it back:

docker login <public-ip> -u admin
docker pull alpine:latest
docker tag alpine:latest <public-ip>/team/alpine:1.0
docker push <public-ip>/team/alpine:1.0
docker pull <public-ip>/team/alpine:1.0

The screenshot below shows a complete round trip against this registry. docker login succeeds with the per-instance credential, and docker push uploads the image layers. After the local copy is removed, docker pull retrieves the identical image from the registry, with a matching digest.

8

How the registry is wired

The registry configuration at /etc/docker/registry/config.yml uses the filesystem storage driver, with its root directory on the dedicated data disk (/var/lib/registry). It binds the HTTP API to loopback only.

nginx is the only network-facing surface. It terminates TLS and enforces htpasswd Basic auth on /v2/. It proxies to the registry with the headers container clients require (Host, X-Forwarded-*, unlimited body size and chunked transfer for large layers, and the Docker-Distribution-Api-Version header).

grep -E 'rootdirectory|addr:|enabled: true|storagedriver' /etc/docker/registry/config.yml
grep -nE 'listen 443 ssl|ssl_certificate|client_max_body_size|chunked_transfer|auth_basic|Docker-Distribution|proxy_pass|return 301' /etc/nginx/sites-available/sac-registry
9

Use your own domain and a trusted certificate (production)

The self-signed certificate is a convenience for getting started. For production, front the registry with your own DNS name and a CA-signed certificate, so clients trust it without any certs.d step.

Point a DNS record (for example registry.example.com) at the VM public IP.

Replace /etc/nginx/tls/registry.crt and /etc/nginx/tls/registry.key with your CA-signed certificate and key (for example from Let's Encrypt or your internal CA). Then run sudo nginx -t && sudo systemctl reload nginx.

Clients can then run docker login registry.example.com with no certificate trust step.

10

Scale the storage backend

For larger deployments, Distribution supports object storage backends. To store blobs in Azure Blob Storage or S3 instead of the local data disk, edit the storage section of /etc/docker/registry/config.yml. Replace the filesystem driver with azure or s3 and add its credentials, then run sudo systemctl restart registry.

See the Distribution documentation for the full set of storage driver options.

11

Server components

  • Registry: CNCF Distribution v3.1.1 (official release binary, sha256 pinned)
  • Registry binding: 127.0.0.1:5000 (loopback only, no authentication at this layer)
  • Front proxy: nginx (TLS on :443, htpasswd Basic auth, :80 redirects to HTTPS with a 301)
  • Storage: filesystem driver, root /var/lib/registry on a dedicated 20 GiB data disk
  • Credential: per-instance admin password, generated on first boot, in /root/registry-credentials.txt (0600)
  • TLS certificate: self-signed, regenerated per VM on first boot (SAN = public IP and hostname), in /etc/nginx/tls/
  • Operating system: Ubuntu 24.04 LTS (patched at build time)
  • License: Apache-2.0 (Distribution)
12

Managing the registry service

Use these commands to check, restart and follow the registry service, and to reload nginx:

sudo systemctl status registry
sudo systemctl restart registry
sudo journalctl -u registry -f sudo systemctl reload nginx

Image blobs live under /var/lib/registry on the data disk. They persist across service restarts and VM reboots.

13

Security recommendations

  • Restrict the NSG. Allow TCP 443 (and 22 for administration) only from the client networks that need to push and pull. Do not expose the registry to the public internet unless you intend to.
  • Rotate the credential. To change the admin password, regenerate the htpasswd entry with sudo htpasswd -B /etc/nginx/.htpasswd admin, then run sudo systemctl reload nginx. Update /root/registry-credentials.txt to match.
  • Add more users. Grant additional accounts with sudo htpasswd -B /etc/nginx/.htpasswd <username>, then reload nginx.
  • Use a trusted certificate (Step 9) for production, so clients validate the registry without a manual trust step.
  • Keep the OS patched. Unattended security upgrades remain enabled on the running VM.
  • Back up /var/lib/registry (the data disk) to preserve your pushed images.
14

Support and Licensing

Docker Registry is built on the CNCF Distribution project, distributed under the Apache License 2.0. This SecureAnyCloud image bundles the unmodified official release binary.

SecureAnyCloud provides the packaging, hardening, per-instance credential and TLS automation, and 24/7 support with a guaranteed 24-hour response SLA.

Deploy on Azure

Find Docker Registry (Distribution) 3.1 on Ubuntu 24.04 in the Azure Marketplace, published by SecureAnyCloud. Deploy from the Portal or the Azure CLI as shown above.

Need Help?

Email sales@secureanycloud.com for deployment help, configuration questions or licensing inquiries.

Deploy on Azure

Launch Docker Registry (Distribution) 3.1 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure