Configure Caddy web server to automatically discover backend services through Consul, enabling dynamic load balancing without manual configuration updates. This setup provides high availability and automatic failover for microservices architectures.
Prerequisites
- Root or sudo access
- At least 2GB RAM
- Network connectivity for downloading packages
- Basic understanding of web servers and load balancing
What this solves
Dynamic service discovery eliminates manual load balancer configuration when services scale up, down, or relocate. Caddy with Consul integration automatically detects healthy backend services and updates routing rules in real-time, preventing downtime during service changes.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions.
sudo apt update && sudo apt upgrade -y
Install required packages
Install curl and unzip for downloading and extracting Caddy and Consul binaries.
sudo apt install -y curl unzip wget gpg
Install Consul
Download and install the latest Consul binary from HashiCorp's official releases. Consul will handle service discovery and health checking.
cd /tmp
wget https://releases.hashicorp.com/consul/1.17.0/consul_1.17.0_linux_amd64.zip
unzip consul_1.17.0_linux_amd64.zip
sudo mv consul /usr/local/bin/
sudo chmod +x /usr/local/bin/consul
Create Consul user and directories
Create a dedicated system user for Consul and set up necessary directories with proper permissions.
sudo useradd --system --home /etc/consul.d --shell /bin/false consul
sudo mkdir -p /etc/consul.d /opt/consul /var/lib/consul
sudo chown consul:consul /etc/consul.d /opt/consul /var/lib/consul
sudo chmod 755 /etc/consul.d /opt/consul /var/lib/consul
Configure Consul server
Create the main Consul configuration file with server settings, data directory, and client access configuration.
datacenter = "dc1"
data_dir = "/var/lib/consul"
log_level = "INFO"
server = true
bootstrap_expect = 1
bind_addr = "0.0.0.0"
client_addr = "0.0.0.0"
retry_join = ["127.0.0.1"]
ui_config {
enabled = true
}
connect {
enabled = true
}
ports {
grpc = 8502
}
acl = {
enabled = false
default_policy = "allow"
}
Create Consul systemd service
Set up a systemd service file to manage Consul as a system service with automatic startup and proper security settings.
[Unit]
Description=Consul
Documentation=https://www.consul.io/
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty=/etc/consul.d/consul.hcl
[Service]
Type=notify
User=consul
Group=consul
ExecStart=/usr/local/bin/consul agent -config-dir=/etc/consul.d/
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Start and enable Consul
Start the Consul service and enable it to start automatically on boot.
sudo chown consul:consul /etc/consul.d/consul.hcl
sudo systemctl daemon-reload
sudo systemctl enable consul
sudo systemctl start consul
sudo systemctl status consul
Install Caddy
Install Caddy web server using the official installation script which adds the repository and GPG key.
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
Configure Caddy with Consul service discovery
Create a Caddyfile that uses Consul for automatic service discovery and health checks. This configuration sets up dynamic upstream discovery for web services.
{
admin localhost:2019
order consul first
}
example.com {
reverse_proxy {
dynamic consul {
service "web-service"
refresh_interval 5s
}
health_uri /health
health_interval 10s
health_timeout 5s
}
log {
output file /var/log/caddy/access.log
format json
}
}
api.example.com {
reverse_proxy {
dynamic consul {
service "api-service"
refresh_interval 5s
}
health_uri /api/health
health_interval 10s
health_timeout 5s
}
}
Create log directory for Caddy
Create a log directory with proper permissions for Caddy to write access logs.
sudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy
sudo chmod 755 /var/log/caddy
Install Caddy Consul plugin
Download and install the Caddy binary with the Consul plugin for service discovery functionality.
cd /tmp
wget https://github.com/pteich/caddy-tlsconsul/releases/download/v0.2.0/caddy-consul-linux-amd64.tar.gz
tar -xzf caddy-consul-linux-amd64.tar.gz
sudo systemctl stop caddy
sudo cp caddy /usr/bin/caddy
sudo chmod 755 /usr/bin/caddy
sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/caddy
Configure firewall rules
Open necessary ports for Caddy (80, 443) and Consul (8500, 8600) communication.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8500/tcp
sudo ufw allow 8600/tcp
sudo ufw allow 8600/udp
Start Caddy service
Start Caddy and enable it to start automatically on boot. Verify the configuration is valid before starting.
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl enable caddy
sudo systemctl start caddy
sudo systemctl status caddy
Register sample services with Consul
Register test services in Consul to demonstrate service discovery functionality. This creates web services that Caddy can automatically discover and route to.
{
"ID": "web-service-1",
"Name": "web-service",
"Tags": ["web", "primary"],
"Address": "203.0.113.10",
"Port": 8080,
"Check": {
"HTTP": "http://203.0.113.10:8080/health",
"Interval": "10s",
"Timeout": "3s"
}
}
{
"ID": "web-service-2",
"Name": "web-service",
"Tags": ["web", "secondary"],
"Address": "203.0.113.11",
"Port": 8080,
"Check": {
"HTTP": "http://203.0.113.11:8080/health",
"Interval": "10s",
"Timeout": "3s"
}
}
curl -X PUT --data @/tmp/web-service1.json http://127.0.0.1:8500/v1/agent/service/register
curl -X PUT --data @/tmp/web-service2.json http://127.0.0.1:8500/v1/agent/service/register
Configure health check endpoints
Add health check configuration to Caddy for monitoring backend service health and automatic failover.
{
admin localhost:2019
order consul first
servers {
metrics
}
}
Main application load balancing
example.com {
reverse_proxy {
dynamic consul localhost:8500 {
service web-service
refresh_interval 30s
}
health_path /health
health_interval 30s
health_timeout 10s
health_status 2xx
# Load balancing policy
lb_policy least_conn
# Fail timeout
fail_timeout 30s
max_fails 3
}
# Enable access logging
log {
output file /var/log/caddy/example.com.log {
roll_size 100MiB
roll_keep 5
}
format json
}
# Enable compression
encode gzip
}
API service load balancing
api.example.com {
reverse_proxy {
dynamic consul localhost:8500 {
service api-service
refresh_interval 30s
}
health_path /api/v1/health
health_interval 30s
health_timeout 10s
lb_policy round_robin
fail_timeout 30s
max_fails 2
}
}
Reload Caddy configuration
Apply the updated configuration and verify Caddy is running with the new settings.
sudo caddy reload --config /etc/caddy/Caddyfile
sudo systemctl status caddy
Verify your setup
Check that Consul is discovering services and Caddy is routing traffic correctly.
# Check Consul cluster status
consul members
List registered services
consul catalog services
Check specific service health
consul health service web-service
Verify Caddy is running
sudo systemctl status caddy
Check Caddy admin API
curl http://127.0.0.1:2019/config/
Test service discovery
curl -H "Host: example.com" http://127.0.0.1/
Access the Consul web UI at http://your-server-ip:8500 to monitor services and health checks visually.
Configure advanced health checks
Add custom health check scripts
Create advanced health check scripts for more sophisticated service monitoring beyond simple HTTP checks.
#!/bin/bash
Advanced health check for web service
set -e
SERVICE_URL="http://localhost:8080"
HEALTH_ENDPOINT="${SERVICE_URL}/health"
METRICS_ENDPOINT="${SERVICE_URL}/metrics"
Check HTTP response
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_ENDPOINT" || echo "000")
if [ "$HTTP_CODE" -ne 200 ]; then
echo "Health check failed: HTTP $HTTP_CODE"
exit 1
fi
Check if service is responsive within 2 seconds
RESPONSE_TIME=$(curl -s -w "%{time_total}" -o /dev/null "$HEALTH_ENDPOINT")
if (( $(echo "$RESPONSE_TIME > 2.0" | bc -l) )); then
echo "Service too slow: ${RESPONSE_TIME}s"
exit 1
fi
Check metrics endpoint
METRICS_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$METRICS_ENDPOINT" || echo "000")
if [ "$METRICS_CODE" -ne 200 ]; then
echo "Metrics endpoint failed: HTTP $METRICS_CODE"
exit 1
fi
echo "Service healthy"
exit 0
sudo mkdir -p /opt/consul/health-checks
sudo chmod +x /opt/consul/health-checks/web-service-check.sh
sudo chown consul:consul /opt/consul/health-checks/web-service-check.sh
Register services with script-based checks
Update service definitions to use custom health check scripts for more comprehensive monitoring.
{
"services": [
{
"id": "web-service-1",
"name": "web-service",
"tags": ["web", "primary", "v1.2.0"],
"address": "203.0.113.10",
"port": 8080,
"meta": {
"version": "1.2.0",
"environment": "production"
},
"checks": [
{
"id": "web-http-check",
"name": "HTTP API Check",
"http": "http://203.0.113.10:8080/health",
"method": "GET",
"interval": "10s",
"timeout": "3s",
"deregister_critical_service_after": "30s"
},
{
"id": "web-script-check",
"name": "Advanced Health Check",
"script": "/opt/consul/health-checks/web-service-check.sh",
"interval": "30s",
"timeout": "10s"
}
]
}
]
}
sudo chown consul:consul /etc/consul.d/web-services.json
sudo systemctl reload consul
Implement automatic failover
Configure Caddy upstream selection
Set up advanced upstream selection policies and failover behavior in Caddy for high availability.
{
admin localhost:2019
order consul first
servers {
metrics
}
}
Production traffic with intelligent failover
example.com {
reverse_proxy {
dynamic consul localhost:8500 {
service web-service
refresh_interval 10s
# Only use healthy services
health_check {
uri /health
interval 15s
timeout 5s
status 2xx
body contains "ok"
}
}
# Load balancing configuration
lb_policy ip_hash # Sticky sessions
lb_try_duration 30s
lb_try_interval 250ms
# Health and failover
health_uri /health
health_interval 15s
health_timeout 5s
health_status 200
# Failure handling
fail_timeout 30s
max_fails 3
unhealthy_request_count 5
# Passive health checking
passive_health {
unhealthy_status 5xx
unhealthy_count 3
unhealthy_duration 30s
healthy_count 2
healthy_duration 10s
}
# Circuit breaker pattern
transport http {
dial_timeout 5s
response_header_timeout 10s
expect_continue_timeout 1s
keepalive_idle_conns 100
keepalive_idle_conns_per_host 10
}
}
# Error pages for upstream failures
handle_errors {
@5xx expression {http.error.status_code} >= 500
rewrite @5xx /errors/5xx.html
file_server {
root /var/www/errors
}
}
log {
output file /var/log/caddy/example.com.log {
roll_size 100MiB
roll_keep 10
}
format json {
time_format "iso8601"
message_key "msg"
}
level INFO
}
encode gzip zstd
}
Health check endpoint for load balancer
example.com/lb-health {
respond "OK" 200 {
headers {
Content-Type "text/plain"
X-Health-Check "caddy-consul"
}
}
}
Create error pages
Set up custom error pages for upstream failures to provide better user experience during outages.
sudo mkdir -p /var/www/errors
sudo chown caddy:caddy /var/www/errors
Service Temporarily Unavailable
Service Temporarily Unavailable
We're experiencing technical difficulties. Please try again in a few minutes.
If the problem persists, please contact support.
Apply configuration and test failover
Reload Caddy with the new configuration and test the failover mechanism.
sudo caddy reload --config /etc/caddy/Caddyfile
Test failover by stopping one service
consul services deregister -id="web-service-1"
Check remaining services
consul health service web-service
Re-register the service
curl -X PUT --data @/tmp/web-service1.json http://127.0.0.1:8500/v1/agent/service/register
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Consul agent won't start | Configuration syntax error | consul validate /etc/consul.d/consul.hcl to check config |
| Services not appearing in Consul | Registration failed or network issues | Check curl http://127.0.0.1:8500/v1/agent/services and service logs |
| Caddy shows no upstreams | Consul connectivity or service query issues | Verify consul catalog services and check Caddy logs |
| Health checks failing | Service endpoints not responding | Test endpoints manually: curl http://service-ip:port/health |
| Load balancing not working | All services marked unhealthy | Check consul health service service-name and fix health endpoints |
| Caddy permission denied on ports | Missing CAP_NET_BIND_SERVICE capability | Run sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/caddy |
| Services deregistering unexpectedly | Health check timeout too short | Increase check interval and timeout in service definition |
Next steps
- Configure HAProxy with Consul for dynamic service discovery and automatic backend updates
- Install and configure Consul for service discovery with clustering and security
- Implement Consul ACL security and encryption for production deployments
- Setup Caddy automatic SSL certificates with Let's Encrypt and DNS challenges
- Monitor Caddy and Consul integration with Prometheus and Grafana dashboards
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
# Configuration
readonly CONSUL_VERSION="1.17.0"
readonly CONSUL_USER="consul"
readonly CONSUL_HOME="/etc/consul.d"
readonly CONSUL_DATA_DIR="/var/lib/consul"
readonly CONSUL_LOG_DIR="/var/log/consul"
readonly CADDY_LOG_DIR="/var/log/caddy"
# Default values
DOMAIN="${1:-example.com}"
API_DOMAIN="${2:-api.example.com}"
# Usage message
usage() {
echo "Usage: $0 [domain] [api_domain]"
echo "Example: $0 mysite.com api.mysite.com"
exit 1
}
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
# Cleanup function for rollback
cleanup() {
log_error "Installation failed. Cleaning up..."
systemctl stop consul caddy 2>/dev/null || true
systemctl disable consul caddy 2>/dev/null || true
userdel -r $CONSUL_USER 2>/dev/null || true
rm -rf $CONSUL_HOME $CONSUL_DATA_DIR $CONSUL_LOG_DIR $CADDY_LOG_DIR 2>/dev/null || true
rm -f /usr/local/bin/consul /etc/systemd/system/consul.service 2>/dev/null || true
}
trap cleanup ERR
# Detect distribution
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewall-cmd"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
FIREWALL_CMD="firewall-cmd"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution"
exit 1
fi
}
# Check prerequisites
check_prerequisites() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$ ]] && [[ "$DOMAIN" != "example.com" ]]; then
log_error "Invalid domain format: $DOMAIN"
usage
fi
}
# Update system packages
update_system() {
echo "[1/10] Updating system packages..."
$PKG_UPDATE
}
# Install required packages
install_packages() {
echo "[2/10] Installing required packages..."
$PKG_INSTALL curl unzip wget
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL gpg debian-keyring debian-archive-keyring apt-transport-https
else
$PKG_INSTALL gpg
fi
}
# Install Consul
install_consul() {
echo "[3/10] Installing Consul..."
cd /tmp
wget -q "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip"
unzip -q "consul_${CONSUL_VERSION}_linux_amd64.zip"
mv consul /usr/local/bin/
chmod 755 /usr/local/bin/consul
rm -f "consul_${CONSUL_VERSION}_linux_amd64.zip"
}
# Create Consul user and directories
create_consul_user() {
echo "[4/10] Creating Consul user and directories..."
useradd --system --home $CONSUL_HOME --shell /bin/false $CONSUL_USER || true
mkdir -p $CONSUL_HOME $CONSUL_DATA_DIR $CONSUL_LOG_DIR
chown $CONSUL_USER:$CONSUL_USER $CONSUL_HOME $CONSUL_DATA_DIR $CONSUL_LOG_DIR
chmod 755 $CONSUL_HOME $CONSUL_DATA_DIR $CONSUL_LOG_DIR
}
# Configure Consul
configure_consul() {
echo "[5/10] Configuring Consul..."
cat > $CONSUL_HOME/consul.hcl << 'EOF'
datacenter = "dc1"
data_dir = "/var/lib/consul"
log_level = "INFO"
log_file = "/var/log/consul/consul.log"
server = true
bootstrap_expect = 1
bind_addr = "0.0.0.0"
client_addr = "127.0.0.1"
ui_config {
enabled = true
}
connect {
enabled = true
}
ports {
grpc = 8502
}
acl = {
enabled = false
default_policy = "allow"
}
EOF
chown $CONSUL_USER:$CONSUL_USER $CONSUL_HOME/consul.hcl
chmod 640 $CONSUL_HOME/consul.hcl
}
# Create Consul systemd service
create_consul_service() {
echo "[6/10] Creating Consul systemd service..."
cat > /etc/systemd/system/consul.service << EOF
[Unit]
Description=Consul
Documentation=https://www.consul.io/
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty=$CONSUL_HOME/consul.hcl
[Service]
Type=notify
User=$CONSUL_USER
Group=$CONSUL_USER
ExecStart=/usr/local/bin/consul agent -config-dir=$CONSUL_HOME/
ExecReload=/bin/kill -HUP \$MAINPID
KillMode=process
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
chmod 644 /etc/systemd/system/consul.service
}
# Install Caddy
install_caddy() {
echo "[7/10] Installing Caddy..."
if [[ "$PKG_MGR" == "apt" ]]; then
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update
$PKG_INSTALL caddy
else
$PKG_INSTALL 'dnf-command(copr)' || true
dnf copr enable -y @caddy/caddy
$PKG_INSTALL caddy
fi
}
# Configure Caddy
configure_caddy() {
echo "[8/10] Configuring Caddy with Consul service discovery..."
mkdir -p $CADDY_LOG_DIR
chown caddy:caddy $CADDY_LOG_DIR
chmod 755 $CADDY_LOG_DIR
cat > /etc/caddy/Caddyfile << EOF
{
admin localhost:2019
}
$DOMAIN {
reverse_proxy {
to consul+http://127.0.0.1:8500/web-service
health_uri /health
health_interval 10s
health_timeout 5s
}
log {
output file $CADDY_LOG_DIR/access.log
format json
}
}
$API_DOMAIN {
reverse_proxy {
to consul+http://127.0.0.1:8500/api-service
health_uri /health
health_interval 10s
health_timeout 5s
}
log {
output file $CADDY_LOG_DIR/api-access.log
format json
}
}
EOF
chown root:caddy /etc/caddy/Caddyfile
chmod 640 /etc/caddy/Caddyfile
}
# Start services
start_services() {
echo "[9/10] Starting and enabling services..."
systemctl daemon-reload
systemctl enable consul caddy
systemctl start consul
sleep 5
systemctl start caddy
}
# Configure firewall
configure_firewall() {
echo "[10/10] Configuring firewall..."
if command -v ufw >/dev/null 2>&1; then
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 8500/tcp
ufw --force enable || log_warn "UFW may already be enabled"
elif command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-port=8500/tcp
firewall-cmd --reload
fi
}
# Verify installation
verify_installation() {
echo "Verifying installation..."
if systemctl is-active --quiet consul; then
log_info "✓ Consul is running"
else
log_error "✗ Consul is not running"
return 1
fi
if systemctl is-active --quiet caddy; then
log_info "✓ Caddy is running"
else
log_error "✗ Caddy is not running"
return 1
fi
if curl -s http://127.0.0.1:8500/v1/status/leader >/dev/null; then
log_info "✓ Consul API is responsive"
else
log_warn "⚠ Consul API may not be ready yet"
fi
log_info "Installation completed successfully!"
echo "Consul UI: http://$(hostname -I | awk '{print $1}'):8500"
echo "Configure your services to register with Consul on 127.0.0.1:8500"
echo "Services should register with names 'web-service' and 'api-service'"
}
# Main execution
main() {
check_prerequisites
detect_distro
update_system
install_packages
install_consul
create_consul_user
configure_consul
create_consul_service
install_caddy
configure_caddy
start_services
configure_firewall
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh