Deployment guide

Caddy 2

Overview

Caddy is a modern open source web server with automatic HTTPS from Let's Encrypt built in. It needs no Certbot and no manual certificate steps. Add a domain to the Caddyfile and reload. Caddy provisions a real certificate on the first request and renews it automatically.

This SecureAnyCloud image runs Caddy 2.x on Ubuntu 24.04 on Azure, installed from the official Caddy team Cloudsmith APT repo. The default Caddyfile serves the SecureAnyCloud landing page on plain HTTP port 80. A comment block at the top of the file shows how to swap in your domain and turn on automatic HTTPS in one edit.

What you get:

  • Caddy 2.x server installed from the official Caddy team Cloudsmith APT repo
  • Default Caddyfile at /etc/caddy/Caddyfile serving the SecureAnyCloud landing page on TCP 80
  • Document root at /var/www/html
  • caddy.service systemd unit that starts on boot and runs as the caddy system user
  • caddy-firstboot.service systemd oneshot that writes endpoint info on the first boot
  • Listeners on TCP 80 (HTTP), TCP 443 (HTTPS once you configure a domain), and TCP 2019 (admin API, on 127.0.0.1 only by default)
  • 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

  • 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 and a DNS A record that points at the VM public IP. Let's Encrypt automatic HTTPS requires both, because Caddy proves domain ownership with 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, so scale up only for very high request rates or large reverse-proxy fleets.

1

Deploy from the Azure Portal

In the Azure Portal, open Marketplace and search for Caddy 2. Select the SecureAnyCloud publisher entry and choose 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, and TCP 443 from your client networks. The Let's Encrypt HTTP-01 challenge and renewal require port 80. Keep port 80 open from the internet so that Caddy can issue and renew certificates.

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

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-sac/providers/Microsoft.Compute/galleries/sacGallery/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
3

Connect via SSH

ssh azureuser@<vm-ip>

caddy.service is already running, and caddy-firstboot.service has already written the endpoint info file.

4

Verify the Caddy Service

sudo systemctl status caddy.service --no-pager

The service should show active (running), with listeners on :80 (public) and :2019 (loopback only).

Confirm the firstboot sentinel:

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

Confirm the listener on port 80:

sudo ss -tln | grep ':80 '

Confirm that the admin API is bound to localhost only:

sudo ss -tln | grep ':2019 '

The output 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, so never expose it beyond the loopback interface.

5

Verify the Default Site

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

The output shows the SecureAnyCloud landing HTML, and the caddy version reports v2.x:

6

Enable Automatic HTTPS for Your Domain

This is the workflow Caddy is built around: three edits, one reload, and a real certificate in about a minute.

Part A. Point your DNS A record at the VM public IP. Make this change at your DNS provider (Route 53, Cloudflare or GoDaddy). Confirm propagation:

dig +short www.example.com

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

Part B. Edit the Caddyfile.

The following shows the shipped default Caddyfile and a caddy validate pass:

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 the file and exit.

Part C. 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. Later requests reuse the cached certificate. Renewal runs silently in the background, and Caddy automatically renews any certificate that is within 30 days of expiry.

7

Verify HTTPS

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

The response should show HTTP/2 200. Inspect the certificate with openssl:

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

The output should show issuer=C = US, O = Let's Encrypt or similar.

8

Reverse Proxy Pattern

Caddy works well as a TLS-terminating reverse proxy. To put HTTPS in front of an internal app server, for example Grafana on port 3000, use:

www.example.com { reverse_proxy localhost:3000 }

To add custom headers, gzip and a request body limit, use:

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} } }

Run sudo systemctl reload caddy, and Caddy reloads the configuration with no downtime.

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/sac/data/caddy-firstboot.done

Inspect installed version:

HOME=/root caddy version

The HOME=/root prefix suppresses Caddy's "no $HOME" warning when you run it outside an interactive shell.

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 enforces its own indentation style):

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

Admin API (Localhost Only)

Caddy includes a programmable admin API on 127.0.0.1:2019. It is useful for automation, but never bind it to a public interface without putting authentication in front of it.

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

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

To reload from a JSON config file instead of using systemctl reload:

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

Troubleshooting

Cannot reach Caddy on port 80

  • Confirm that the service is running: sudo systemctl status caddy.service
  • Confirm that the listener is bound: sudo ss -tln | grep ':80 '
  • Check the journal: sudo journalctl -u caddy.service --no-pager -n 50
  • Confirm that the NSG allows TCP 80 from your client source IP

Automatic HTTPS not working

  • Confirm that port 80 is open from the internet. The Let's Encrypt HTTP-01 challenge requires it
  • Confirm that the DNS A record points at the VM public IP: dig +short <your-domain>
  • Check the Caddy log for ACME errors: sudo journalctl -u caddy.service --no-pager | grep -i acme | tail -10
  • If you exceeded the 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 the journal: sudo journalctl -u caddy.service --no-pager -n 50
  • Check filesystem space: df -h /var/lib/caddy
  • Verify that the caddy user owns the document root: sudo ls -la /var/www/html
13

Security Recommendations

  • Restrict port 22 to your management IP ranges only.
  • Keep port 80 open from the internet. The Let's Encrypt HTTP-01 challenge and renewal require it. 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, which is the default, and never proxy it through a public interface.
  • Keep Caddy updated. Run sudo apt-get update && sudo apt-get upgrade caddy periodically. The official Caddy Cloudsmith repo is already configured.
  • Monitor Caddy access logs through journald (journalctl -u caddy.service), or add a log directive to the Caddyfile to ship logs to a file or remote syslog.
14

Support and Licensing

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

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

Email: sales@secureanycloud.com

Website: secureanycloud.com

Support hours: 24/7 with a guaranteed 24-hour response SLA

Deploy on Azure

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

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure

Launch Caddy 2 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