Compiling Caddy from Source #
By default, the Caddy installer packages you get through package managers (like APT on Ubuntu or COPR on RHEL) and the official Docker images only include core modules. These built-in modules are more than enough for common web server needs: standard reverse proxying, static file serving, Gzip/Brotli compression, and regular HTTPS certificate negotiation via ACME HTTP/TLS challenges.
However, in more complex production architectures, you often need additional features that aren’t provided by default. The most common example is issuing a Wildcard SSL certificate (*.yoursite.com), which requires validating domain ownership using the DNS-01 challenge. This scenario needs a special DNS provider module (like Cloudflare, Route53, or DigitalOcean). Another example is adding rate limiting, centralized authentication (OAuth2/OIDC), or a distributed caching system.
To meet these needs, Caddy provides a very easy self-compilation mechanism using the official utility called xcaddy. This article guides you through compiling Caddy from source code safely and professionally.
Why Compile Caddy Yourself? #
Let’s compare the feature availability between the standard official binary and custom plugin needs:
Official Caddy Binary (Ready to Use):
✓ http.handlers.reverse_proxy (Reverse proxy)
✓ http.handlers.file_server (Static files)
✓ http.handlers.encode (Gzip, Zstd)
✓ http.handlers.basicauth (Basic authentication)
✓ http.handlers.rewrite & redirect
✓ tls.issuance.acme (Let's Encrypt / ZeroSSL)
✓ tls.issuance.internal (Local CA)
Additional Plugins (Need Compilation):
✗ dns.providers.cloudflare → Reads Cloudflare DNS for SSL
✗ dns.providers.route53 → AWS Route53 integration
✗ dns.providers.digitalocean → DigitalOcean DNS integration
✗ http.handlers.rate_limit → Per-IP/User request limiting
✗ http.handlers.security → SSO / OAuth2 / OIDC integration
✗ http.handlers.cache → Advanced HTTP cache system
If your architecture needs any of the components in the right column, you must compile it yourself.
Prerequisite: Installing the Go Compiler #
Because Caddy is written in Go, the compilation process requires the Go Compiler on your build system. Caddy always needs a relatively recent Go version (at least Go 1.21 or later).
[!IMPORTANT] Don’t Compile on the Production Server! Running the Go compiler requires substantial CPU and memory resources. Additionally, installing development tools on a production server expands the security attack surface. The best practice is to compile on a developer’s local machine or a dedicated build server (CI/CD), then ship the compiled binary to the production server.
If you want to set up Go in your build environment (for example, an Ubuntu build machine):
# Step 1: Clean up any old Go installation
sudo rm -rf /usr/local/go
# Step 2: Download the official Go package (adjust to the latest version)
GO_VER="1.22.4"
wget "https://go.dev/dl/go${GO_VER}.linux-amd64.tar.gz"
# Step 3: Extract the package to the /usr/local directory
sudo tar -C /usr/local -xzf "go${GO_VER}.linux-amd64.tar.gz"
# Step 4: Register the Go path in your shell configuration (~/.bashrc or ~/.zshrc)
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
echo 'export PATH=$PATH:$(go env GOPATH)/bin' >> ~/.bashrc
# Step 5: Reload the shell configuration
source ~/.bashrc
# Step 6: Verify that the Go compiler is installed correctly
go version
# Expected output: go version go1.22.4 linux/amd64
Installing the xcaddy Utility
#
The Caddy developer team built xcaddy to hide the complexity of long Go build commands. xcaddy automatically downloads the Caddy source code for the version you want, downloads the plugins you define, assembles the initialization code, and compiles everything into a single complete binary file.
You can install xcaddy globally using the go install command:
# Download and compile the latest xcaddy directly from the official repository
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
The xcaddy executable is stored in the $GOPATH/bin directory (usually at ~/go/bin). Make sure you’ve registered that path in your shell environment variables (per Step 4 in the prerequisites section).
Verify the xcaddy installation with:
xcaddy version
# Example output: xcaddy v0.4.2
Compiling Caddy with xcaddy
#
The compilation process with xcaddy is highly modular. You can specify the main Caddy version you want to build and include one or more plugins at once.
1. Standard Build (Without Additional Plugins) #
To simply confirm your compiler works, do a standard Caddy build:
# Start building the latest stable Caddy version
xcaddy build
This command produces an executable file named caddy in your current working directory. You can check its size and internal module list:
# Check the binary file exists
ls -lh caddy
# Check your custom binary's version
./caddy version
# List all modules integrated into the binary
./caddy list-modules
2. Build with the Cloudflare DNS Plugin #
Let’s build Caddy with the Cloudflare DNS module for Wildcard SSL management:
# Build Caddy with the cloudflare plugin included
xcaddy build \
--with github.com/caddy-dns/cloudflare
When finished, verify the DNS module is active:
./caddy list-modules | grep cloudflare
# Output: dns.providers.cloudflare
3. Build with Multiple Plugins and a Specific Version #
For production needs, you’re strongly advised to specify the Caddy major/minor version explicitly so builds are consistently repeatable (reproducible build):
# Build Caddy v2.8.4 with several custom modules at once
xcaddy build v2.8.4 \
--with github.com/caddy-dns/cloudflare \
--with github.com/mholt/caddy-ratelimit \
--with github.com/caddyserver/cache-handler
Pinning Plugin Versions for Production Security #
By default, if you don’t specify a plugin version when running --with, xcaddy looks up the latest version of that plugin on GitHub. This behavior is not safe for production environments because future API changes in the plugin code can break your build out of nowhere.
You must pin each plugin to a specific semver release tag or commit hash:
# CORRECT: Pin the Caddy version and each plugin version precisely
xcaddy build v2.8.4 \
--with github.com/caddy-dns/[email protected] \
--with github.com/mholt/[email protected]
You can wrap this build command in a simple shell script so your developer team has consistent build documentation:
# Open an editor to create the build script
nano build-caddy.sh
Fill it with the following script:
#!/usr/bin/env bash
# build-caddy.sh
# Script for consistently compiling a custom Caddy.
set -euo pipefail
CADDY_VER="v2.8.4"
CLOUDFLARE_VER="v0.0.0-20240101123456-abc1234def56"
RATELIMIT_VER="v0.0.0-20240101234567-def5678abc12"
echo "=== STARTING CADDY COMPILATION ==="
xcaddy build "${CADDY_VER}" \
--with "github.com/caddy-dns/cloudflare@${CLOUDFLARE_VER}" \
--with "github.com/mholt/caddy-ratelimit@${RATELIMIT_VER}"
echo "=== VERIFYING BUILD RESULTS ==="
./caddy version
./caddy list-modules | grep -E 'cloudflare|rate_limit'
echo "Compilation successful!"
Save the script and make it executable:
chmod +x build-caddy.sh
./build-caddy.sh
Deploying the New Binary to a Systemd Server #
After getting your custom caddy binary from your build machine, the next step is replacing the old Caddy binary on your production server.
APT Binary Replacement Scenario (Rollback Plan) #
If your production server previously used an APT installation (where the default binary lives at /usr/bin/caddy), follow this maintenance sequence to minimize downtime:
# Step 1: Temporarily stop the Caddy service
sudo systemctl stop caddy
# Step 2: Back up the old binary with a date marker
# This backup is crucial so you can roll back quickly if the new binary crashes
sudo cp /usr/bin/caddy /usr/bin/caddy.backup.$(date +%Y%m%d)
# Step 3: Copy your new custom binary to the /usr/bin/ directory
sudo cp ./caddy /usr/bin/caddy
# Step 4: Grant execute permission on the new binary
sudo chmod +x /usr/bin/caddy
# Step 5: Verify the version and plugins at the system path
caddy version
caddy list-modules | grep cloudflare
# Step 6: Restart the Caddy service
sudo systemctl start caddy
# Step 7: Check the journalctl logs to ensure the transition went smoothly
sudo journalctl -u caddy -n 20 --no-pager
If a fatal problem occurs when the service runs with the new binary, you can restore the server to its previous state quickly:
# QUICK ROLLBACK STRATEGY (In Case of Failure)
sudo cp /usr/bin/caddy.backup.[BACKUP_DATE] /usr/bin/caddy
sudo systemctl start caddy
CI/CD Integration Using GitHub Actions #
For large teams, automating Caddy compilation with a CI/CD pipeline is the best solution. Every time you release a new tag, GitHub Actions automatically compiles and stores the result as a release artifact.
Create a workflow file at .github/workflows/build-caddy.yml:
name: Build Custom Caddy
on:
push:
tags:
- 'v*'
workflow_dispatch: # Allows manual triggering from the GitHub dashboard
jobs:
compile:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Go Compiler
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install xcaddy
run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
- name: Compile Caddy Binary
run: |
xcaddy build v2.8.4 \
--with github.com/caddy-dns/[email protected] \
--with github.com/mholt/[email protected]
- name: Upload Binary Artifact
uses: actions/upload-artifact@v4
with:
name: caddy-custom-linux-amd64
path: ./caddy
Compiling for Local Plugin Development #
If you’re developing your own Caddy plugin in Go, you can use xcaddy for cross-compilation by pointing the module at your local directory.
Say you have your custom plugin project folder:
your-plugin-project/
├── go.mod
├── go.sum
└── our_plugin.go
You can compile Caddy including that local code using the equals sign (=):
# Map the Go package name to your local physical directory
xcaddy build \
--with github.com/your-name/caddy-our-plugin=./your-plugin-project
This makes prototyping and debugging Caddy modules in real time easy before publishing them to GitHub.
Compilation Troubleshooting #
1. Go Version Problem: “Requires Go 1.21 or Later” #
The compilation stops because the Go compiler version on your system is too old. Solution: Remove the Go installation bundled with your Linux distro’s package manager (which usually lags a few versions behind) and install the latest Go version manually from golang.org following the guide at the beginning of this article.
2. xcaddy: Command Not Found #
The shell can’t find the xcaddy command after the go install process.
Solution:
Make sure the GOPATH environment variable is registered and its bin folder is included in your system’s global PATH variable.
# Add this to your shell
export PATH=$PATH:$(go env GOPATH)/bin
3. Module Version Incompatibilities (Dependency Conflicts) #
The build produces Go compile-time errors related to incompatible parameter types or module structures.
Solution:
This happens because the plugin version you’re calling uses a different internal Caddy API than the Caddy version you specified. Try removing the specific @version marker on the plugin to test compilation with the latest version, which is generally aligned with the latest stable Caddy release.
When to Switch to Alternatives / Not Use This #
Keep using xcaddy compilation if:
✓ You need a DNS provider module for Let's Encrypt Wildcard SSL validation.
✓ You want to deploy third-party security plugins (rate limit, OAuth).
✓ You're developing your own Caddy plugin locally.
Consider other methods if:
✗ You only need Caddy as a standard static web server (Use APT/COPR).
✗ You don't have the access or expertise to maintain a manual binary update cycle.
✗ Organization policy requires strict, integrated OS package dependency tracking.
Summary #
- xcaddy’s Role — The official tool that simplifies assembling a custom Caddy binary by combining the source download and plugin compilation process.
- Wildcard SSL — DNS provider modules must be installed via custom compilation to support wildcard SSL certificate issuance.
- Version Pinning — Always specify specific versions for Caddy and each plugin (
@version) to guarantee production build consistency.- Rollback Strategy — Always back up the old binary before overwriting it with a new one on your production server.
- CI/CD Build — Use pipeline automation (like GitHub Actions) to produce verifiable, secure binary files.
- Server Isolation — Don’t install the Go Compiler on production servers. Build in a separate environment and ship the finished binary to the target server.