Best Practices #

After understanding all the features and modules Caddy has, this closing article summarizes the best practices compiled from the practical experience of the community and production server operators. This document presents a checklist and systematic guide to ensure your Caddy deployment runs reliably, securely, with high performance, and is easy to manage long term.

Caddy Production Readiness Audit Diagram #

Before releasing your Caddy server to the production gate facing direct public internet traffic, you must do a structured production readiness audit according to the flow below:

flowchart TD
    Start["Start the Production Readiness Audit"] --> Security{"1. Security Audit?"}
    
    Security --> SecHSTS["Enable HSTS & Security Headers"]
    SecHSTS --> SecUser["Run Caddy as a Non-Root User"]
    SecUser --> SecAdmin["Restrict the Admin API to Localhost"]
    
    SecAdmin --> Performance{"2. Performance Audit?"}
    
    Performance --> PerfCompression["Enable gzip / zstd"]
    PerfCompression --> PerfCache["Set Static Asset Cache-Control"]
    PerfCache --> PerfLimits["Tune LimitNOFILE & somaxconn"]
    
    PerfLimits --> Monitoring{"3. Monitoring Audit?"}
    
    Monitoring --> MonLogs["Separate Access & Error Logs"]
    MonLogs --> MonProm["Enable Prometheus Metrics"]
    MonProm --> MonAlert["Set Up Downtime Alerting Flow"]
    
    MonAlert --> Backup{"4. Backup Audit?"}
    
    Backup --> BackAuto["Schedule Caddyfile & Certs Backups"]
    BackAuto --> BackTest["Simulate the Rollback Procedure"]
    
    BackTest --> Ready["Caddy Ready for Production Launch (Go-Live)"]
    
    style Start stroke:#0288d1,stroke-width:2px
    style Ready stroke:#2e7d32,stroke-width:2px

1. Security #

Security is the most important aspect that can’t be negotiated in production environments. Caddy is already very secure by default thanks to automatic TLS management, but you still must close security gaps at the application and OS configuration levels.

Global Options and Security Headers #

Always configure the responsible email so Let’s Encrypt can send notifications if there’s a certificate renewal failure. Additionally, restrict Admin API access and hide your server’s fingerprint traces:

# Global Options Configuration
{
    # Main email for emergency certificate notifications
    email [email protected]
    
    # Restrict the Admin API to only be accessible from localhost (Default)
    # NEVER set it to 0.0.0.0 without a firewall or strict authentication!
    admin localhost:2019
}

example.com {
    # 1. Enable HSTS (Strict-Transport-Security)
    # Forces client browsers to only use HTTPS. Test with a small max-age first.
    header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    
    # 2. Hide the server identity for security (Obfuscation)
    header {
        -Server
        -X-Powered-By
    }
    
    # 3. Standard Security Headers
    header {
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        X-XSS-Protection "1; mode=block"
    }
    
    reverse_proxy localhost:3000
}

Following the Principle of Least Privilege #

Caddy must not be run directly using the root user identity. If the Caddy binary is compromised, attackers immediately get full control over your entire OS.

  • Dedicated System User: Run Caddy under the system user caddy and group caddy that don’t have shell login rights.
  • Port Restriction: Give the cap_net_bind_service capability so the caddy user can bind ports 80/443 without needing root access.
  • File Permissions: Make sure the Caddyfile can only be read by the caddy user (mode 640 or rw-r-----), not world-readable.
# Set safe access permissions on the configuration directory
sudo chown -R root:caddy /etc/caddy
sudo chmod 750 /etc/caddy
sudo chmod 640 /etc/caddy/Caddyfile

2. Performance #

Caddy is written in Go, which has very high concurrency performance using the goroutine mechanism. You can drastically increase server throughput through smart compression, caching tactics, and tuning resource limits on the Linux OS.

Dynamic Compression and Static Asset Cache Policies #

Enabling data compression significantly reduces transfer packet sizes, which directly speeds up the Largest Contentful Paint (LCP) metric on client web browsers.

example.com {
    # Enable compression with priority order: zstd (most efficient) followed by gzip
    encode gzip zstd
    
    root * /var/www/html
    
    # Aggressive Caching for Hashed Assets (Vite / Webpack output)
    # CSS/JS files with unique hashes will never change their contents
    @hash_assets path_regexp \.[a-f0-9]{8,}\.(js|css|woff2?|png|jpg)$
    handle @hash_assets {
        header Cache-Control "public, max-age=31536000, immutable"
        file_server
    }
    
    # Don't cache main HTML files so browsers always detect the latest release
    @html_files path *.html /
    handle @html_files {
        header Cache-Control "no-cache, no-store, must-revalidate"
        file_server
    }
    
    # Fallback for regular static assets
    file_server
}

Linux OS Capacity Limit Tuning #

By default, Linux limits the number of files one process can open (File Descriptor Limit) to 1024. Because every client TCP connection is considered one file by the OS, a Caddy server with high traffic quickly triggers the Too many open files error.

Raise this limit through the Systemd unit override for the Caddy process:

sudo systemctl edit caddy

Add the following lines to the override file:

[Service]
# Raise the maximum number of simultaneously open files
LimitNOFILE=65535

Next, optimize the Linux kernel TCP socket queue by updating the /etc/sysctl.conf configuration:

# Increase the incoming connection queue capacity in the kernel
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
# Widen the local port range for outgoing proxy connections
net.ipv4.ip_local_port_range = 1024 65535
# Speed up cleaning sockets in TIME_WAIT status
net.ipv4.tcp_fin_timeout = 15

Apply the sysctl changes directly without rebooting:

sudo sysctl -p

3. Monitoring and Observability #

You can’t fix what you don’t measure. Production servers must have a structured logging system and automatic early warning (alerting) systems.

Automatic Network Health Test Script (Downtime Alerting) #

Use the following simple bash script run through a daily cron to quickly detect service outages and send alerts:

#!/bin/bash
# caddy-health-alert.sh — Service health monitor with Slack notifications
set -u

URLS=(
    "https://example.com"
    "https://api.example.com/health"
)
SLACK_WEBHOOK="https://hooks.slack.com/services/T000/B000/XXXXXX"

for url in "${URLS[@]}"; do
    # Do the request with a maximum 10-second timeout
    HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" --max-time 10 "$url" || echo "000")
    
    if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "301" ]; then
        msg="🚨 *CADDY DOWNTIME ALARM* 🚨\nTarget: $url\nHTTP Status: $HTTP_STATUS\nTime: $(date)"
        
        # Send the notification to the operations team Slack channel
        curl -s -X POST "$SLACK_WEBHOOK" \
            -H "Content-Type: application/json" \
            -d "{\"text\": \"$msg\"}"
    fi
done

Schedule it in the root crontab to run every 5 minutes:

*/5 * * * * /usr/local/bin/caddy-health-alert.sh > /dev/null 2>&1

4. Backup and Disaster Recovery #

SSL/TLS certificates and Caddyfile configuration files are valuable assets. Losing this data due to disk failure causes a total outage because Caddy must re-request all certificates from scratch, which can trigger Let’s Encrypt rate limits.

Automatic Configuration and Certificate Backup Script #

#!/bin/bash
# backup-caddy-production.sh — Periodically backup Caddy important files
set -euo pipefail

BACKUP_DIR="/var/backups/caddy"
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"

echo "[+] Backing up the Caddyfile configuration..."
cp /etc/caddy/Caddyfile "$BACKUP_DIR/Caddyfile.$DATE"

echo "[+] Backing up the live JSON configuration from runtime memory..."
curl -s http://localhost:2019/config/ | jq . > "$BACKUP_DIR/config-live.$DATE.json" || echo "Warning: Live API not reachable"

echo "[+] Compressing the local SSL certificate folder..."
# For standard Linux installations, certificates are stored under the caddy user
tar -czf "$BACKUP_DIR/caddy-certs-$DATE.tar.gz" -C /var/lib/caddy/.local/share/caddy/ certificates/

# Delete backups older than 30 days to save disk space
find "$BACKUP_DIR" -mtime +30 -type f -delete

echo "[✓] Backup successfully created at: $BACKUP_DIR"

Disaster Recovery Procedure (Disaster Recovery Runbook) #

If the Caddy server experiences a total hardware failure and you must launch a new server instance from scratch, follow this step-by-step recovery guide diligently:

  1. Caddy Binary Installation: Install the Caddy binary on the new server using your OS’s official package repository, or use a custom single binary if you use additional modules.
  2. Prepare the Directory Structure and Users: Create the system caddy user and caddy group (usually automatically created during package manager installation), then prepare the /var/lib/caddy/ and /etc/caddy/ folders.
  3. Restore SSL Certificates: Download the latest caddy-certs-*.tar.gz backup archive, extract it to its original location at /var/lib/caddy/.local/share/caddy/, then run the sudo chown -R caddy:caddy /var/lib/caddy/ command. This step is crucial to prevent blocking errors due to Let’s Encrypt rate limits if the new server requests mass certificate re-creation from scratch.
  4. Restore the Caddyfile: Copy the latest Caddyfile configuration file back to /etc/caddy/Caddyfile, then make sure its ownership is set to root:caddy with 640 access permissions.
  5. Validate and Run the Service: Test the file validation using caddy validate --config /etc/caddy/Caddyfile. If valid, turn the service back on via systemd by running sudo systemctl start caddy and monitor the system logs to make sure there are no TLS handshake errors.

5. Upgrade Strategy #

Doing Caddy binary version upgrades in production environments must be done very carefully to avoid breaking custom module compatibility.

  1. Use a Staging Environment: Always install the new Caddy version on a staging server first. Run validation and load tests to detect memory leaks.
  2. Validate Syntax with the New Binary: Before replacing the active binary, use the new binary to validate the current production Caddyfile:
    ./caddy-new validate --config /etc/caddy/Caddyfile
    
  3. Graceful Switch: Caddy is designed with dynamic binary replacement capability. You can just overwrite the old /usr/bin/caddy binary file with the new binary, then run the systemctl reload caddy command. Systemd sends a graceful reload signal, Caddy does an atomic swap of the configuration to the new binary in memory without cutting off active client connection sockets.

6. Document Your Configuration #

The Caddyfile is living documentation. Always include comments on non-obvious decisions configuration sections so your teammates or future you don’t struggle to make modifications.

Example of Good Caddyfile Comment Documentation #

# ==============================================================================
# PRODUCTION API GATEWAY CONFIGURATION
# Main Domain: api.example.com
# Last Updated: 2026-06-16 by the Infrastructure Team
# Design References: RFC 8594 (Deprecation Headers) & HSTS Preload
# ==============================================================================
{
    email [email protected]
    admin localhost:2019
}

api.example.com {
    # ── SECURITY POLICIES ──
    # Enabling HSTS Preload after passing the 30-day staging trial.
    # All subdomains must use HTTPS.
    header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    
    # ── COMPRESSION ──
    # Avoiding double compression on image files and zip archives
    encode gzip zstd
    
    # ── ROUTING: USER SERVICE ──
    # Backend Port: 3001 (Node.js Express App)
    handle /api/v1/users* {
        uri strip_prefix /api/v1
        reverse_proxy user-service:3001 {
            # Forward the real client IP through AWS CloudFront
            header_up X-Real-IP {remote_host}
        }
    }
}

7. Production Checklist #

Here’s the final checklist to check before declaring your Caddy server ready for go-live:

SECURITY:
  □ HTTPS is running with a valid production certificate (Let's Encrypt / ZeroSSL).
  □ The global 'email' option is configured with an active email address.
  □ Basic security headers (HSTS, nosniff, DENY) are enabled centrally.
  □ Server system identities ('Server', 'X-Powered-By') are removed from responses.
  □ The Admin API is restricted to localhost only (localhost:2019) and not exposed.
  □ Caddy runs as a regular (non-root) user with minimal capabilities.
  □ The Caddyfile is protected with chmod 640 file permissions (only readable by caddy).

PERFORMANCE:
  □ zstd & gzip compression is enabled on all dynamic responses.
  □ Aggressive Cache-Control headers are set for hashed static assets (.js, .css).
  □ The maximum open files limit (LimitNOFILE) is set to 65535 in Systemd.
  □ The Linux kernel tcp queue tuning (somaxconn) has been increased.
  □ Reverse proxy timeouts are set explicitly to prevent clogged connections.

MONITORING:
  □ Structured access logs (JSON) are active and directed to external files.
  □ Log rotation is configured so log file sizes don't fill storage memory.
  □ The Prometheus metrics endpoint is enabled on the local admin port.
  □ Automatic downtime monitoring health check scripts are active in cron.

BACKUP & RECOVERY:
  □ Automatic daily backups for the Caddyfile & local certificates run in cron.
  □ The quick rollback procedure (rollback runbook) has been tested in staging.

Closing #

Congratulations! You’ve completed the entire series of administration, configuration, and architecture guides for the Caddy Web Server. From Caddyfile syntax basics, microservice reverse proxy integration, automatic HTTPS management, to production optimization and advanced troubleshooting tactics.

Caddy is an outstanding web server software for the modern era — hiding complexity behind the elegance of simple configuration writing, without sacrificing performance and system architecture flexibility. The Caddy community is very friendly and active at caddy.community if you need further help or want to contribute to the development of this modern web server.

Happy building reliable and secure infrastructure!


Summary #

  • Security First — Never run the Caddy binary as root in production. Use the isolated system caddy user and restrict the Admin API to only be accessible through localhost.
  • Cache Optimization — Differentiate cache policies between hashed static assets (public, max-age=31536000, immutable) and main HTML files (no-cache) for maximum performance.
  • OS Network Tuning — Raise Caddy’s LimitNOFILE limit to 65535 in Systemd to avoid running out of sockets when handling high traffic.
  • Data File Backup — Always back up the SSL certificate directory /var/lib/caddy/ periodically to avoid Let’s Encrypt rate limits in disaster scenarios.
  • CI/CD Validation Scripts — Apply the caddy validate command on git pre-commit hooks and CI/CD pipelines to ensure wrong configurations never reach the server.
  • Graceful Upgrade — Do graceful Caddy binary upgrades leveraging the Caddy reload command without damaging or cutting off active user connections.
← Previous: Diagnostic Tools
About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact