Applications Azure

Docker Registry (Distribution) 3.1 on Ubuntu 24.04 on Azure User Guide

| Product: Docker Registry (Distribution) 3.1 on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of Docker Registry (Distribution) 3.1 on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. 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 lets you distribute OCI and Docker container images.

The image 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 — the registry is a self contained server. The registry binary binds to loopback only (127.0.0.1:5000) and runs with no registry level authentication; nginx is the sole network facing surface and terminates TLS on port 443 while enforcing HTTP Basic authentication against a bcrypt htpasswd file.

Security by design — no anonymous access. A registry with no authentication is an open push and pull surface for anyone who can reach it. On the very first boot, this image generates a per instance admin credential (a random openssl rand -base64 24 secret) and writes it into the nginx htpasswd file, and it regenerates a fresh self signed TLS certificate for this specific VM (the certificate Subject Alternative Names include the VM public IP and hostname). Anonymous requests to the registry API return HTTP 401 with a WWW-Authenticate: Basic challenge; only the per instance credential is accepted. The credential is written to /root/registry-credentials.txt (mode 0600, root only).

What is included:

  • 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 network exposed

  • 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 returns 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) — no anonymous access, no shared default password

  • A self signed TLS certificate regenerated per VM on first boot (SAN includes the VM public IP + hostname) — replace it with your own CA signed certificate for production

  • Image blobs stored on a dedicated 20 GiB Azure data disk mounted at /var/lib/registry — your pushed images survive independently of the OS disk and the volume is independently resizable

  • Ubuntu 24.04 LTS base with latest security patches applied at build time

  • Azure Linux Agent for seamless cloud integration and SSH key injection

  • 24/7 cloudimg support with guaranteed 24 hour response SLA

Prerequisites

  • Active Azure subscription, SSH public key, VNet + subnet in target region

  • Subscription to the Docker Registry (Distribution) listing on 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) for a team registry and light workloads. Registries serving many concurrent pulls or storing large images should use Standard_D4s_v5 (4 vCPU, 16 GB RAM) or larger, and a larger data disk.

Step 1: Deploy from the Azure Portal

Search Docker Registry in Marketplace, select the cloudimg publisher, click Create. NSG rules: TCP 22 (admin), TCP 443 (registry API over HTTPS), and TCP 80 (HTTP to HTTPS redirect and health probe) from your client networks.

Step 2: Deploy from the Azure CLI

RG="registry-prod"; LOCATION="eastus"; VM_NAME="registry-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/docker-registry-ubuntu-24-04/versions/<version>"
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
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
  --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

Step 3: Connect via SSH and retrieve your registry credential

The per instance admin credential is generated on the first boot and stored 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) and the per instance password, plus quick start commands. Store the password somewhere safe — it is unique to this VM.

Step 4: Verify the registry and nginx services

Confirm both services are active and that the registry is bound to loopback only. Note in the listener output that 127.0.0.1:5000 (the registry) is loopback only, while nginx listens publicly on :443 and :80:

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

registry.service and nginx.service both active (running); ss shows the registry bound to 127.0.0.1:5000 loopback only, with nginx public on :443 and :80

Step 5: Check the health endpoint

The /healthz endpoint is served by nginx without authentication (ideal for load balancer and Azure health probes) and returns HTTP 200:

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

Step 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 (no credential) is rejected with HTTP 401. The self signed certificate lives at /etc/nginx/tls/registry.crt, so we 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; the anonymous request returns HTTP 401. There is no anonymous access to this registry.

curl of /v2/_catalog and /v2/team/hello/tags/list with the admin credential returns JSON; an anonymous request to /v2/ returns HTTP 401 Unauthorized from nginx

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, docker push uploads the image layers, and after removing the local copy, docker pull retrieves the identical image back from the registry (matching digest).

A complete docker login + docker push + docker pull round trip against the registry: login succeeds with the per instance admin credential, the image is pushed, removed locally, then pulled back with a matching sha256 digest

Step 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) and binds the HTTP API to loopback only. nginx is the only network facing surface: it terminates TLS, enforces htpasswd Basic auth on /v2/, and 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/cloudimg-registry

config.yml showing filesystem rootdirectory /var/lib/registry and http addr 127.0.0.1:5000, plus the nginx directives: listen 443 ssl, ssl_certificate, client_max_body_size 0, chunked_transfer_encoding on, auth_basic, the Docker-Distribution-Api-Version header, and proxy_pass to 127.0.0.1:5000

Step 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 then simply docker login registry.example.com with no certificate trust step.

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 its credentials), then sudo systemctl restart registry. See the Distribution documentation for the full set of storage driver options.

Step 11: Server components

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

Step 12: Managing the registry service

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 and persist across service restarts and VM reboots.

Step 13: Security recommendations

  • Restrict the NSG. Allow TCP 443 (and 22 for admin) 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: sudo htpasswd -B /etc/nginx/.htpasswd admin then sudo systemctl reload nginx. Update /root/registry-credentials.txt accordingly.

  • Add more users. Grant additional accounts with sudo htpasswd -B /etc/nginx/.htpasswd <username> and 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.

Step 14: Support and Licensing

Docker Registry is built on the CNCF Distribution project, distributed under the Apache License 2.0. This cloudimg image bundles the unmodified official release binary. cloudimg 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 on the Azure Marketplace, published by cloudimg. Deploy from the Portal or the Azure CLI as shown above.

Need Help?

Email support@cloudimg.co.uk for deployment help, configuration questions, or licensing enquiries.