Deployment guide

Zot

Overview

Zot is a production-ready, vendor-neutral, OCI-native container image registry from the project-zot community. It stores OCI artifacts and container images, and it lets you push and pull with any standard client, including docker, podman, skopeo and oras, in a small single-binary footprint.

This SecureAnyCloud image runs Zot 2.1.17 as a systemd service behind an nginx reverse proxy on Azure. Registry data lives on a dedicated data disk, and every VM generates its own unique admin account on first boot.

What you get

  • Zot 2.1.17 (Apache-2.0), the full build with the bundled web UI and the search and CVE-scan extensions.
  • The registry API and the web UI published on port 80 through nginx, with WebSocket upgrade and unlimited body size for large image-layer pushes.
  • htpasswd (bcrypt) authentication on every endpoint. The OCI base endpoint /v2/ returns 401 until you authenticate.
  • A dedicated Azure data disk at /var/lib/zot for the OCI blob storage, the configuration and the htpasswd authentication database.
  • A unique admin account generated on first boot and written to a root-only credentials file.
  • zot.service and nginx.service as enabled, active systemd units.
  • 24/7 SecureAnyCloud support.

Prerequisites

Before you deploy, make sure you have an active Azure subscription, an SSH key pair, and a VNet and subnet in the target region.

Standard_B2ms (2 vCPU / 8 GiB RAM) is a good starting point. In the NSG, allow inbound 22/tcp from your management network and 80/tcp for the registry and web UI. For public exposure, put TLS in front of the registry (see Enabling HTTPS).

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Zot by SecureAnyCloud, and select Create.

On the Basics tab, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTP (80).

Review the dedicated data disk on the Disks tab, then select Review + create, then Create.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:zot-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name zot \
  --image sac1647283583153:zot-ubuntu-24-04:default:latest \
  --size Standard_B2ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Then open the registry and web UI port on the VM's network security group:

az vm open-port --resource-group <your-rg> --name zot --port 80 --priority 1001

Step 3 - Retrieve the per-VM admin password

Every VM generates a unique admin account on first boot and writes it to a root-only credentials file. SSH in and read the file:

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

The file contains the registry URL, the admin username (admin) and the generated password:

ZOT_URL=http://<vm-public-ip>/ ZOT_ADMIN_USER=admin ZOT_ADMIN_PASSWORD=<ZOT_ADMIN_PASSWORD>

Keep this password safe. It is the credential for the web UI and for docker and podman login.

Step 4 - Confirm the registry is healthy

The OCI base endpoint /v2/ requires authentication. It returns 401 without credentials and 200 once you authenticate as admin. Run these commands on the VM:

echo "Unauthenticated /v2/ (expect 401):" curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/v2/
PASS=$(sudo grep '^ZOT_ADMIN_PASSWORD=' /root/zot-credentials.txt | cut -d= -f2-)
echo "Authenticated /v2/ (expect 200):" curl -s -o /dev/null -w '%{http_code}\n' -u "admin:${PASS}" http://127.0.0.1/v2/
echo "nginx static health (expect ok):" curl -s http://127.0.0.1/health

Expected output:

Unauthenticated /v2/ (expect 401): 401 Authenticated /v2/ (expect 200): 200 nginx static health (expect ok): ok

Check that the systemd services are active:

systemctl is-active zot.service nginx.service

Expected output:

active active

Step 5 - Browse the web UI

Open http://<vm-public-ip>/ in a browser and sign in with admin and the password from Step 3. The home page lists the container image repositories hosted on this registry.

  • Select a repository to see its tags, size and metadata.
  • Open a tag to inspect its manifest, its layers and the vulnerability (CVE) scan results produced by the bundled search extension.
  • Use the explore and search view to find repositories and images across the registry, filtered by operating system and architecture.

Step 6 - Log in and push an image from a developer machine

On any machine with docker or podman installed, log in to the registry with the admin credentials, tag a local image for the registry, and push it. Replace <vm-public-ip> and <ZOT_ADMIN_PASSWORD> with your values.

With docker:

docker login <vm-public-ip> --username admin --password '<ZOT_ADMIN_PASSWORD>'
docker pull alpine:3.20
docker tag alpine:3.20 <vm-public-ip>/library/alpine:3.20
docker push <vm-public-ip>/library/alpine:3.20

With podman, the flow is identical:

podman login <vm-public-ip> --username admin --password '<ZOT_ADMIN_PASSWORD>' podman pull docker.io/library/busybox:1.36 podman tag docker.io/library/busybox:1.36 <vm-public-ip>/library/busybox:1.36 podman push <vm-public-ip>/library/busybox:1.36

The image serves plain HTTP on port 80 until you add TLS (see Enabling HTTPS), so docker and podman treat the registry as insecure. For a quick test, add <vm-public-ip> to insecure-registries in /etc/docker/daemon.json (docker). For podman, add it to [[registry]] insecure = true in /etc/containers/registries.conf. In production, enable TLS and this workaround is unnecessary.

Step 7 - Pull an image back and list the catalog

Pull the image you pushed back down on any client:

docker pull <vm-public-ip>/library/alpine:3.20

On the VM, list every repository in the registry through the OCI catalog API:

PASS=$(sudo grep '^ZOT_ADMIN_PASSWORD=' /root/zot-credentials.txt | cut -d= -f2-)
curl -s -u "admin:${PASS}" http://127.0.0.1/v2/_catalog

Example output (your repositories differ):

{"repositories":["library/alpine","library/busybox"]}

Step 8 - Push OCI artifacts with skopeo or oras

Zot is a generic OCI registry. Copy images with skopeo, or push arbitrary OCI artifacts with oras:

skopeo copy --dest-creds admin:'<ZOT_ADMIN_PASSWORD>' \
  docker://docker.io/library/redis:7 \
  docker://<vm-public-ip>/library/redis:7 oras push <vm-public-ip>/sac/notes:v1 \
  --username admin --password '<ZOT_ADMIN_PASSWORD>' \
  ./release-notes.txt:text/plain

Storage on a dedicated data disk

The OCI blob storage, the configuration and the htpasswd authentication database live on a dedicated Azure data disk mounted at /var/lib/zot (rootDirectory in /etc/zot/config.json). The disk is separate from the OS disk, so you can resize it independently, and it is re-provisioned with every VM. Confirm the mount and the free space on the VM:

findmnt -no SOURCE,TARGET,FSTYPE /var/lib/zot df -h /var/lib/zot | tail -1

Expected output (the device name may differ):

/dev/sdc /var/lib/zot ext4 /dev/sdc 40G 24K 38G 1% /var/lib/zot

To grow registry storage, resize the data disk in the Azure Portal or with az disk update --size-gb. Then grow the filesystem on the VM with sudo resize2fs /dev/disk/azure/scsi1/lun0.

Enabling HTTPS

Zot serves plain HTTP on port 80 through nginx. For production, terminate TLS at nginx with a certificate for your own domain. Point an A record at the VM's public IP, then install a certificate with certbot and the nginx plugin. Run the commands on the VM and follow the interactive prompts.

Install certbot with sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx, then run sudo certbot --nginx -d your-domain.example.com. Certbot edits the nginx site to listen on 443 with your certificate and sets up automatic renewal.

After TLS is in place, docker login and podman login no longer need the insecure-registry workaround.

Maintenance

  • OS updates: the image keeps Ubuntu's unattended-upgrades enabled, so security patches continue to land. Apply pending updates manually at any time with sudo apt-get update && sudo apt-get upgrade -y.
  • Service logs: run sudo journalctl -u zot.service -f to follow the registry log, and sudo journalctl -u nginx.service for the proxy.
  • Restart: run sudo systemctl restart zot.service nginx.service.
  • Garbage collection: Zot is configured with dedupe and gc enabled, so deleted manifests and unreferenced blobs are reclaimed automatically.
  • Rotating the admin password: generate a new bcrypt entry with sudo htpasswd -B /var/lib/zot/htpasswd admin. Zot reads the htpasswd file at request time, so no restart is needed.

Support

SecureAnyCloud maintains this image and provides 24/7 support. For help, contact sales@secureanycloud.com.

Deploy on Azure

Launch Zot OCI Registry 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