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.servicesystemd unit that starts on boot and runs as thecaddysystem usercaddy-firstboot.servicesystemd 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.1only 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.
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.
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 StandardConnect via SSH
ssh azureuser@<vm-ip>caddy.service is already running, and caddy-firstboot.service has already written the endpoint info file.
Verify the Caddy Service
sudo systemctl status caddy.service --no-pagerThe 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_DONEConfirm 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.
Verify the Default Site
curl -s http://localhost/ | head -10The output shows the SecureAnyCloud landing HTML, and the caddy version reports v2.x:
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.comThe 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/CaddyfileReplace 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 caddyCaddy 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.
Verify HTTPS
curl -sI https://www.example.com/ | head -5The 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 -datesThe output should show issuer=C = US, O = Let's Encrypt or similar.
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.
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 versionThe HOME=/root prefix suppresses Caddy's "no $HOME" warning when you run it outside an interactive shell.
Managing the Caddy Service
Status:
sudo systemctl status caddy.service --no-pagerStop / Start / Restart:
sudo systemctl stop caddy.service
sudo systemctl start caddy.service
sudo systemctl restart caddy.serviceReload config without dropping connections:
sudo systemctl reload caddyValidate config before reload:
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfileView logs:
sudo journalctl -u caddy.service --no-pager -n 50Format the Caddyfile (Caddy enforces its own indentation style):
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo systemctl reload caddyAdmin 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 500To 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/loadTroubleshooting
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 caddyfilefor a precise error message - Run
sudo caddy fmt /etc/caddy/Caddyfileto 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
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.azureplugin 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 caddyperiodically. The official Caddy Cloudsmith repo is already configured. - Monitor Caddy access logs through journald (
journalctl -u caddy.service), or add alogdirective to the Caddyfile to ship logs to a file or remote syslog.
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