xcaddy

xcaddy is awesome for setting up your LetsEncrypt certs with a DNS challenge (handy if you don't often - or ever - want to expose port 80/443 to the www)

Install xcaddy

# Get regular caddy first so xcaddy compiles right
wget https://github.com/caddyserver/caddy/releases/download/v2.11.0-beta.1/caddy_2.11.0-beta.1_linux_amd64.tar.gz
tar -xzvf caddy_2.11.0-beta.1_linux_amd64.tar.gz

# Then download/extract xcaddy
wget https://github.com/caddyserver/xcaddy/releases/download/v0.4.5/xcaddy_0.4.5_linux_amd64.tar.gz
tar -xzvf xcaddy_0.4.5_linux_amd64.tar.gz

# Download/install go
sudo apt install golang -y

# Build the custom xcaddy
./xcaddy build \
  --with github.com/mholt/caddy-dynamicdns \
  --with github.com/caddy-dns/cloudflare

Get a CloudFlare API token

Do that here:

  1. Click Create Token.
  2. Under API token templates, look for Edit Zone DNS and click Use Template
  3. At the next screen, leave Permissions settings as is, and under Zone Resources, leave the default of Include and Specific Zone and then in the blank drop-down box, choose the domain you want to assign the token to.
  4. (Optional) Under Client IP address filtering add the IP(s) you want to limit token use to.

Create the Caddy file

Here's an example:

some.cool.site.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }

    reverse_proxy reverse_proxy http://127.0.0.1:8000

    log {
        output file /var/log/caddy/yoursiteslog.log
    }
}

Run xcaddy

CF_API_TOKEN=xxx sudo --preserve-env=CF_API_TOKEN ./caddy run --config ./Caddyfile

Troubleshooting

Token changes in summer 2026 (?)

In summer of 2026, a bunch of my LetsEncrypt-enabled Caddy configs had certs that started to expire. After reading this post I discovered I needed to update the xcaddy plugins:

./xcaddy build \
  --with github.com/mholt/caddy-dynamicdns \
  --with github.com/caddy-dns/cloudflare

Then the cert renewals worked!

Upgrade golang

When I was trying to compile xcaddy, I got complaints about not being able to compile the github.com/mholt/caddy-dynamicdns component. The fix was:

# 1) grab latest 1.25.x (adjust version if you want)
cd /tmp
wget https://go.dev/dl/go1.25.5.linux-amd64.tar.gz

# 2) install to /usr/local/go
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.25.5.linux-amd64.tar.gz

# 3) ensure your shell uses it
echo 'export PATH=/usr/local/go/bin:$PATH' >> ~/.profile
source ~/.profile

go version