Applications Azure

Caddy 2 on Ubuntu 24.04 on Azure User Guide

| Product: Caddy 2 on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of Caddy 2 on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Caddy is the modern open-source web server with automatic HTTPS via Let's Encrypt baked in — no Certbot dependency, no manual cert ceremony. Drop a domain into the Caddyfile, reload, and Caddy provisions a real cert on the first request and auto-renews it forever.

The image ships Caddy 2.x from the official Caddy team Cloudsmith APT repo. The default Caddyfile serves the cloudimg landing page on plain HTTP port 80; a comment block at the top shows customers exactly how to swap in their domain to flip on automatic HTTPS in one edit.

What is included:

  • Caddy 2.x server installed from the official Caddy team Cloudsmith APT repo

  • Default Caddyfile at /etc/caddy/Caddyfile serving the cloudimg landing page on TCP 80

  • Document root at /var/www/html

  • caddy.service systemd unit auto-starting on boot, running as the caddy system user

  • caddy-firstboot.service systemd oneshot that writes endpoint info on first customer boot

  • Listeners: TCP 80 (HTTP), TCP 443 (HTTPS once domain configured), TCP 2019 (admin API on 127.0.0.1 only — loopback only by default)

  • 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

  • An active Azure subscription

  • A subscription to the Caddy 2 on Ubuntu 24.04 listing on Azure Marketplace

  • An SSH public key for VM authentication

  • A virtual network and subnet in the target region

  • A registered domain name + DNS A record pointing at the VM public IP (required for Let's Encrypt automatic HTTPS — Caddy proves domain ownership via the HTTP-01 challenge over port 80)

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for typical web traffic. Caddy is efficient — scale up only for very high request rates or large reverse-proxy fleets.

Step 1: Deploy from the Azure Portal

Navigate to Marketplace in the Azure Portal, search for Caddy 2, select the cloudimg publisher entry, and click Create.

On the Networking tab attach a network security group that allows inbound TCP 22 from your management IP range, TCP 80 from 0.0.0.0/0 (required for the Let's Encrypt HTTP-01 challenge and renewal), and TCP 443 from your client networks. Port 80 must remain open from the internet for Caddy to issue and renew certificates.

Click Review + create, wait for validation, then Create. Deployment takes around two minutes.

Step 2: Deploy from the Azure CLI

RG="caddy-prod"
LOCATION="eastus"
VM_NAME="caddy-01"
ADMIN_USER="azureuser"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/caddy-2-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"

az group create --name "$RG" --location "$LOCATION"

az network vnet create \
  --resource-group "$RG" \
  --name caddy-vnet --address-prefix 10.96.0.0/16 \
  --subnet-name caddy-subnet --subnet-prefix 10.96.1.0/24

az network nsg create --resource-group "$RG" --name caddy-nsg

az network nsg rule create \
  --resource-group "$RG" --nsg-name caddy-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 \
  --resource-group "$RG" --nsg-name caddy-nsg \
  --name allow-http-public --priority 110 \
  --source-address-prefixes "*" \
  --destination-port-ranges 80 --access Allow --protocol Tcp

az network nsg rule create \
  --resource-group "$RG" --nsg-name caddy-nsg \
  --name allow-https --priority 120 \
  --source-address-prefixes "*" \
  --destination-port-ranges 443 --access Allow --protocol Tcp

az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$GALLERY_IMAGE_ID" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username "$ADMIN_USER" --ssh-key-values "$SSH_KEY" \
  --vnet-name caddy-vnet --subnet caddy-subnet --nsg caddy-nsg \
  --public-ip-sku Standard

Step 3: Connect via SSH

ssh azureuser@<vm-ip>

caddy.service will already be running and caddy-firstboot.service will already have written the endpoint info file.

Step 4: Verify the Caddy Service

sudo systemctl status caddy.service --no-pager

Expected: active (running) with the listeners on :80 (public) and :2019 (loopback only):

caddy.service active and listening on TCP 80 plus the loopback admin API on TCP 2019

Confirm the firstboot sentinel:

sudo test -f /var/lib/cloudimg/caddy-firstboot.done && echo FIRSTBOOT_DONE

Confirm the listener on port 80:

sudo ss -tln | grep ':80 '

Confirm the admin API is bound to localhost only (defence-in-depth):

sudo ss -tln | grep ':2019 '

The above should show 127.0.0.1:2019 and not 0.0.0.0:2019 or *:2019. The admin API gives full control over the running Caddy and must never be exposed beyond the loopback interface.

Step 5: Verify the Default Site

curl -s http://localhost/ | head -10

You should see the cloudimg landing HTML, and caddy version reports v2.x:

caddy version v2.11.2 with the cloudimg landing page HTML returned by curl http://localhost/

Step 6: Enable Automatic HTTPS for Your Domain

This is the headline workflow Caddy is built around — three edits, one reload, real cert in 60 seconds.

Step 6a — Point your DNS A record at the VM public IP. Done at your DNS provider (Route 53, Cloudflare, GoDaddy). Confirm propagation:

dig +short www.example.com

The output must equal the VM's public IP before continuing.

Step 6b — Edit the Caddyfile.

The shipped default Caddyfile + a caddy validate pass:

Default /etc/caddy/Caddyfile content showing the :80 site block, with caddy validate confirming the syntax is valid

sudo nano /etc/caddy/Caddyfile

Replace the :80 { ... } block with your domain block:

www.example.com {
    root * /var/www/html
    file_server
    encode gzip
}

Save and exit.

Step 6c — Validate then reload.

sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl reload caddy

Caddy provisions a Let's Encrypt cert on the first request to https://www.example.com/, installs it, and serves the site. Subsequent requests reuse the cached cert. Renewal runs silently in the background — Caddy renews any cert ≤30 days from expiry automatically.

Step 7: Verify HTTPS

curl -sI https://www.example.com/ | head -5

You should see HTTP/2 200. Inspect the cert with openssl:

echo | openssl s_client -servername www.example.com -connect www.example.com:443 2>/dev/null | openssl x509 -noout -issuer -subject -dates

You should see issuer=C = US, O = Let's Encrypt (or similar).

Step 8: Reverse Proxy Pattern

Caddy excels at TLS-terminating reverse proxying. To put HTTPS in front of an internal app server (e.g. Grafana on port 3000):

www.example.com {
    reverse_proxy localhost:3000
}

Or with custom headers, gzip, and a request body limit:

www.example.com {
    encode gzip
    request_body {
        max_size 10MB
    }
    reverse_proxy localhost:3000 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
    }
}

sudo systemctl reload caddy and Caddy hot-reloads with no downtime.

Step 9: Server Components

Component Path

Caddy binary /usr/bin/caddy

Config file /etc/caddy/Caddyfile

Document root /var/www/html

Caddy data directory /var/lib/caddy

ACME cert storage /var/lib/caddy/.local/share/caddy/

Systemd unit /lib/systemd/system/caddy.service

Firstboot script /usr/local/sbin/caddy-firstboot.sh

Firstboot service /etc/systemd/system/caddy-firstboot.service

Endpoint info file /stage/scripts/caddy-credentials.log

Firstboot sentinel /var/lib/cloudimg/caddy-firstboot.done

Inspect installed version:

HOME=/root caddy version

(The HOME=/root prefix suppresses Caddy's "no $HOME" warning when running outside an interactive shell.)

Step 10: Managing the Caddy Service

Status:

sudo systemctl status caddy.service --no-pager

Stop / Start / Restart:

sudo systemctl stop caddy.service
sudo systemctl start caddy.service
sudo systemctl restart caddy.service

Reload config without dropping connections:

sudo systemctl reload caddy

Validate config before reload:

sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

View logs:

sudo journalctl -u caddy.service --no-pager -n 50

Format the Caddyfile (Caddy is opinionated about indentation):

sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo systemctl reload caddy

Step 11: Admin API (Localhost Only)

Caddy ships with a programmable admin API on 127.0.0.1:2019. Useful for automation, but never bind it to a public interface without putting auth in front.

Confirm the bind + see a sample of what the API returns:

Admin API bound to 127.0.0.1:2019 only with a sample of the JSON config returned by curl http://127.0.0.1:2019/config/

curl -s http://127.0.0.1:2019/config/ | head -c 500

To reload from a JSON config file (alternative to systemctl reload):

sudo curl -s -X POST -H "Content-Type: application/json" \
    -d @/path/to/config.json http://127.0.0.1:2019/load

Step 12: Troubleshooting

Cannot reach Caddy on port 80

  • Confirm service running: sudo systemctl status caddy.service

  • Confirm listener bound: sudo ss -tln | grep ':80 '

  • Check journal: sudo journalctl -u caddy.service --no-pager -n 50

  • Confirm NSG allows TCP 80 from your client source IP

Automatic HTTPS not working

  • Confirm port 80 is open from the internet (Let's Encrypt HTTP-01 challenge requires it)

  • Confirm DNS A record points at the VM public IP: dig +short <your-domain>

  • Check Caddy log for ACME errors: sudo journalctl -u caddy.service --no-pager | grep -i acme | tail -10

  • If you exceeded Let's Encrypt rate limits (50 certs/week per registered domain), wait and retry

Caddyfile syntax errors

  • Run sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile for a precise error message

  • Run sudo caddy fmt /etc/caddy/Caddyfile to print a properly formatted version side-by-side

Service fails to start

  • Check journal: sudo journalctl -u caddy.service --no-pager -n 50

  • Check filesystem space: df -h /var/lib/caddy

  • Verify caddy user owns the document root: sudo ls -la /var/www/html

Step 13: Security Recommendations

  • Restrict port 22 to your management IP ranges only

  • Keep port 80 open from the internet — required for Let's Encrypt HTTP-01 challenge and renewal. If you cannot expose port 80 publicly, switch to the DNS-01 challenge (Caddy's tls.dns.azure plugin works with Azure DNS)

  • Add Strict-Transport-Security to your domain block: www.example.com { header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" ... }

  • Restrict the admin API (port 2019) to localhost only — the default — and never proxy it through a public interface

  • Keep Caddy updated by running sudo apt-get update && sudo apt-get upgrade caddy periodically; the official Caddy Cloudsmith repo is already configured

  • Monitor Caddy access logs via journald (journalctl -u caddy.service) or by configuring a log directive in the Caddyfile to ship to a file or remote syslog

Step 14: Support and Licensing

Caddy is licensed under the Apache License 2.0. There is no per-domain, per-cert, or per-server fee. Caddy is a registered trademark of ZeroSSL Inc.

cloudimg provides commercial support for this image separately from the upstream project.

  • Email: support@cloudimg.co.uk

  • Website: www.cloudimg.co.uk

  • Support hours: 24/7 with guaranteed 24 hour response SLA

Deploy on Azure

Launch Caddy 2 on Ubuntu 24.04 with 24/7 support from cloudimg.

View on Marketplace

Need Help?

Our support team is available 24/7.

support@cloudimg.co.uk