p 1: Repository Registration & Package Installation
Caddy distributes official binaries through a signed APT repository. Registering the repository ensures package integrity and enables seamless upgrades.
# Refresh local package metadata
sudo apt update
# Import the official signing key
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
sudo gpg --dearmor --output /usr/share/keyrings/caddy-stable-archive-keyring.gpg
# Register the stable repository
echo "deb [signed-by=/usr/share/keyrings/caddy-stable-archive-keyring.gpg] https://dl.cloudsmith.io/public/caddy/stable/debian/deb any main" | \
sudo tee /etc/apt/sources.list.d/caddy-stable.list
# Install the server binary
sudo apt update
sudo apt install caddy -y
# Verify installation integrity
caddy version
Architecture Rationale: Using signed-by in the repository definition prevents keyring conflicts with other APT sources. The caddy package installs the binary to /usr/bin/caddy, creates a dedicated caddy system user, and provisions a pre-configured systemd unit file. This isolation ensures the web server runs with minimal privileges, adhering to the principle of least privilege.
Step 2: Service Lifecycle Management
Caddy ships with a production-ready systemd unit that handles graceful reloads, crash recovery, and capability bounding.
# Enable automatic startup on boot
sudo systemctl enable caddy.service
# Start the service
sudo systemctl start caddy.service
# Verify runtime state
sudo systemctl status caddy.service
Architecture Rationale: The caddy.service unit runs the server as the caddy user, binds to privileged ports using Linux capabilities (CAP_NET_BIND_SERVICE), and restricts filesystem access to /etc/caddy and /var/lib/caddy. This configuration prevents privilege escalation and limits blast radius in the event of a vulnerability.
Step 3: Network & Firewall Configuration
ACME challenges require inbound HTTP traffic on port 80 for domain validation. Port 443 must remain open for production traffic.
# Allow ACME HTTP-01 challenge
sudo ufw allow 80/tcp
# Allow production HTTPS traffic
sudo ufw allow 443/tcp
# Verify firewall state
sudo ufw status
Architecture Rationale: Blocking port 80 prevents Let's Encrypt from completing the HTTP-01 challenge, causing certificate issuance to fail. Caddy automatically handles the challenge lifecycle, but the firewall must permit inbound connections to the validation endpoint.
Step 4: Application Root & Logging Infrastructure
Static assets and access logs require dedicated directories with correct ownership.
# Create document root
sudo mkdir -p /srv/web/portal.internal.dev
sudo chown -R caddy:caddy /srv/web/portal.internal.dev
# Create log directory
sudo mkdir -p /var/log/caddy
sudo chown -R caddy:caddy /var/log/caddy
# Deploy initial content
cat <<EOF | sudo tee /srv/web/portal.internal.dev/index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Internal Portal</title></head>
<body><h1>Service Endpoint Active</h1></body>
</html>
EOF
Architecture Rationale: Separating web roots and logs into /srv and /var/log aligns with Linux filesystem hierarchy standards. Assigning ownership to caddy:caddy ensures the service can read assets and write logs without requiring sudo or elevated permissions.
Step 5: Declarative Routing Configuration
Caddy uses a hierarchical configuration file (Caddyfile) that maps domains to handlers. Replace the default configuration with a production-ready template.
# Backup default configuration
sudo mv /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak
# Create new configuration
sudo nano /etc/caddy/Caddyfile
Insert the following configuration:
portal.internal.dev {
tls admin@internal.dev
root * /srv/web/portal.internal.dev
file_server {
index index.html
}
log {
output file /var/log/caddy/portal.internal.dev.access.log
format console
}
encode gzip
}
Architecture Rationale:
tls: Registers the domain with Let's Encrypt using the provided administrative email. Caddy automatically handles validation, issuance, and renewal.
root *: The wildcard * applies the path to all matchers, preventing routing conflicts.
file_server: Enables static content delivery with directory listing disabled by default.
log: Routes access logs to a dedicated file using a human-readable console format. Production environments should consider JSON formatting for log aggregation pipelines.
encode gzip: Enables response compression, reducing bandwidth and improving latency.
Step 6: Validation & Live Deployment
Never reload Caddy without validating the configuration. Syntax errors during reload can drop active connections.
# Format configuration for consistency
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
# Validate syntax and directive compatibility
sudo caddy validate --config /etc/caddy/Caddyfile
# Apply changes without dropping connections
sudo systemctl reload caddy.service
Architecture Rationale: caddy validate performs a dry-run compilation of the Caddyfile, checking directive compatibility and path existence. systemctl reload sends a SIGUSR1 signal to the running process, which gracefully swaps the configuration while keeping existing connections alive. This zero-downtime reload capability is critical for production environments.
Pitfall Guide
1. Web Root Ownership Mismatch
Explanation: Caddy runs as the caddy user. If the document root is owned by root or another user, the server returns 403 Forbidden errors.
Fix: Always execute sudo chown -R caddy:caddy /path/to/root after creating directories.
2. ACME Challenge Port Blocked
Explanation: Firewalls or cloud security groups that block port 80 prevent Let's Encrypt from completing HTTP-01 validation. Certificate issuance fails silently in logs.
Fix: Explicitly allow inbound TCP traffic on port 80. Verify with sudo ufw status or cloud provider console.
3. Skipping Configuration Validation
Explanation: Reloading Caddy with malformed syntax causes the service to reject the new configuration, potentially leaving the server in an inconsistent state.
Fix: Always run sudo caddy validate --config /etc/caddy/Caddyfile before reloading. Treat validation failures as deployment blockers.
4. Hardcoding Personal Emails in TLS Directive
Explanation: Using a personal email for certificate registration means renewal notices and security alerts bypass team monitoring channels.
Fix: Use a distribution list or team alias (e.g., infra@company.com) to ensure automated alerts reach the responsible engineering group.
5. Misusing the root Directive Syntax
Explanation: Omitting the * matcher or placing root inside a nested block causes routing conflicts and unexpected 404 responses.
Fix: Always use root * /absolute/path at the top level of the site block. Avoid nesting root inside handle or route blocks unless explicitly required for path-specific overrides.
6. Ignoring Log Rotation
Explanation: Caddy writes logs continuously. Without rotation, log files consume disk space and degrade I/O performance over time.
Fix: Enable Caddy's built-in log rotation using roll_size, roll_keep, and roll_keep_days directives, or configure logrotate for /var/log/caddy/*.log.
7. Running Caddy as Root
Explanation: Modifying the systemd unit to run as root bypasses Linux capability restrictions and increases vulnerability exposure.
Fix: Never change User=caddy in the service file. If elevated permissions are required for specific tasks, use Linux capabilities or a separate privileged helper process.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Static marketing site or documentation portal | Caddy with file_server | Zero-config TLS, built-in compression, minimal maintenance | Near-zero operational cost |
| API gateway with dynamic routing | Caddy with reverse_proxy | Declarative matcher syntax, automatic HTTP/2 & HTTP/3, built-in rate limiting | Low infrastructure overhead |
Legacy PHP application with .htaccess dependencies | Nginx + PHP-FPM | Caddy does not support Apache rewrite rules; migration requires refactoring | Moderate migration cost |
| Kubernetes-native microservices | Traefik or Ingress-Nginx | Caddy lacks native CRD support; Kubernetes controllers provide better service discovery | High if forced into K8s without adaptation |
| Multi-tenant SaaS with wildcard domains | Caddy with *.example.com matcher | Automatic certificate provisioning for subdomains, centralized logging | Low per-tenant cost |
Configuration Template
# Production-ready Caddyfile template
# Replace domain, paths, and email before deployment
yourdomain.example.com {
# Automatic TLS provisioning and renewal
tls ops@yourdomain.example.com
# Document root with wildcard matcher
root * /srv/web/yourdomain.example.com
# Static file delivery with default index
file_server {
index index.html
}
# Access logging with rotation
log {
output file /var/log/caddy/yourdomain.example.com.access.log
format console
roll_size 100mb
roll_keep 5
roll_keep_days 30
}
# Response compression
encode gzip
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
Quick Start Guide
- Register Repository & Install: Run the APT key import, repository registration, and
apt install caddy commands. Verify with caddy version.
- Enable Service & Open Ports: Execute
systemctl enable --now caddy.service and allow ports 80/tcp and 443/tcp through your firewall.
- Prepare Directories & Content: Create
/srv/web/yourdomain.example.com and /var/log/caddy, assign caddy:caddy ownership, and place an index.html file.
- Write & Validate Caddyfile: Create
/etc/caddy/Caddyfile with the template, run caddy fmt and caddy validate, then reload with systemctl reload caddy.
- Verify Deployment: Navigate to
https://yourdomain.example.com in a browser. Confirm the TLS padlock appears and access logs populate in /var/log/caddy/.