Skip to content

Caddy Web Server: Complete Configuration Guide

DodaTech Updated 2026-06-23 7 min read

In this tutorial, you'll learn about Caddy Web Server: Complete Configuration Guide. We cover key concepts, practical examples, and best practices.

Caddy is a modern web server written in Go that automatically provisions and renews HTTPS certificates via Let's Encrypt. Its simple Caddyfile syntax and zero-configuration TLS make it the fastest way to deploy a secure production web server.

In this tutorial, you will learn to configure Caddy using the Caddyfile syntax, set up automatic HTTPS, configure reverse proxies to backend services, serve static files, integrate PHP-FPM, and deploy Caddy in production. DodaTech uses Caddy for internal dashboards and Doda Browser telemetry endpoints where quick HTTPS setup is critical.

What You'll Learn

By the end of this guide, you will deploy Caddy as a static file server and reverse proxy, configure automatic HTTPS with Let's Encrypt, enable PHP-FPM for dynamic sites, use Caddy API for dynamic configuration, and harden your Caddy deployment for production.

Why Caddy Matters

Caddy is the only web server that enables HTTPS by default. Every site you configure gets automatic certificates with no extra commands. Its Caddyfile syntax is dramatically simpler than NGINX or Apache configuration. For teams that value developer experience and security, Caddy is the best choice. It works well in DevOps pipelines and integrates with Linux systemd for production deployments.

Caddy Learning Path

flowchart LR
  A[Installation] --> B[Static File Serving]
  B --> C[Reverse Proxy]
  C --> D[PHP-FPM]
  D --> E[Caddy API]
  E --> F{You Are Here}
  style F fill:#f90,color:#fff

Installation

# Install Caddy from the official repository
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 caddy -y

# Verify
sudo systemctl start caddy
sudo systemctl enable caddy
caddy version

Expected output

v2.8.4 h1:...  (version number may vary)

Static File Serving

Caddy serves static files with zero configuration beyond a domain name. Create a Caddyfile:

dodatech.com {
    root * /var/www/dodatech/public
    encode gzip
    file_server
}

Start Caddy:

sudo caddy reload --config /etc/caddy/Caddyfile

Expected behavior

curl -I https://dodatech.com/index.html
# HTTP/2 200
# content-type: text/html
# content-encoding: gzip

Caddy automatically obtains a Let's Encrypt certificate for dodatech.com, redirects HTTP to HTTPS, and compresses responses with gzip. No additional commands needed.

Reverse Proxy

Proxy requests to a backend application server:

api.dodatech.com {
    reverse_proxy 127.0.0.1:3000 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }

    # Rate limiting
    rate_limit {
        zone api_rate {
            key {remote_host}
            events 100
            window 1m
        }
    }

    encode gzip
}

For load balancing across multiple backends:

app.dodatech.com {
    reverse_proxy 10.0.1.1:8080 10.0.1.2:8080 10.0.1.3:8080 {
        lb_policy least_conn
        health_uri /health
        health_interval 30s
        health_timeout 5s
    }
}

Testing the proxy

# Start a test backend
python3 -m http.server 3000 &

# Test proxied request
curl -H "Host: api.dodatech.com" http://localhost
# Response from the backend on port 3000

PHP-FPM Integration

Serve PHP applications with Caddy's php_fastcgi directive:

blog.dodatech.com {
    root * /var/www/blog/public
    encode gzip

    # PHP-FPM on the default socket
    php_fastcgi unix//run/php/php8.3-fpm.sock

    file_server
}

For WordPress or Laravel-style setups:

shop.dodatech.com {
    root * /var/www/shop/public
    encode gzip

    # Try files, then pass to PHP-FPM
    try_files {path} /index.php?{query}

    php_fastcgi unix//run/php/php8.3-fpm.sock {
        env APP_ENV production
        env DB_HOST localhost
    }

    file_server
}

Expected behavior

curl -I https://shop.dodatech.com
# HTTP/2 200
# Powered-By: PHP/8.3

Caddy API for Dynamic Configuration

Caddy's API allows configuration changes without editing files:

# Load a configuration via JSON
curl -X PUT "http://localhost:2019/config/" \
  -H "Content-Type: application/json" \
  -d '{
    "apps": {
      "http": {
        "servers": {
          "example": {
            "listen": [":443"],
            "routes": [
              {
                "handle": [
                  {"handler": "static_response", "body": "Hello from Caddy API"}
                ]
              }
            ]
          }
        }
      }
    }
  }'

# Add a route dynamically
curl -X POST "http://localhost:2019/config/apps/http/servers/example/routes" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": [{
      "handler": "reverse_proxy",
      "upstreams": [{"dial": "127.0.0.1:3000"}]
    }],
    "match": [{"host": ["api.dodatech.com"]}]
  }'

Expected behavior

curl https://api.dodatech.com/status
# {"status":"ok"}  (from the backend on port 3000)

Security and Best Practices

dodatech.com {
    # Directory listing disabled by default
    root * /var/www/dodatech/public

    # Security headers
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
    }

    # Request logging
    log {
        output file /var/log/caddy/access.log
        format json
    }

    # Enforce security
    @blocked {
        path /wp-admin/*
        path /admin/*
        path /\.env
    }
    respond @blocked 404

    encode gzip
    file_server
}

Common Errors

1. Certificate Renewal Fails

Caddy renews certificates automatically, but port 80 must be reachable for the HTTP challenge. Ensure your firewall allows inbound traffic on port 80.

2. Permission Denied on Socket

Caddy runs as the caddy user. PHP-FPM sockets must be readable by the caddy user. Add listen.owner = caddy and listen.group = caddy in www.conf.

3. Reverse Proxy 502 Bad Gateway

The backend is not running or unreachable. Check sudo systemctl status your-backend and verify the upstream address in the Caddyfile.

4. Caddyfile Syntax Error

Use sudo caddy validate --config /etc/caddy/Caddyfile to check syntax before reloading. Caddy reports the exact line number of the error.

5. Port 80 Already in Use

Apache or NGINX may already be binding port 80. Stop the conflicting service with sudo systemctl stop apache2 before starting Caddy.

6. Logs Not Writing

The log directory must be writable by the caddy user. Create the directory: sudo mkdir -p /var/log/caddy && sudo chown caddy:caddy /var/log/caddy.

Practice Questions

1. How does Caddy automatically get HTTPS certificates? Caddy uses the ACME protocol to automatically obtain and renew Let's Encrypt certificates. When you specify a domain in the Caddyfile, Caddy handles the entire certificate lifecycle without manual intervention.

2. What is the difference between reverse_proxy and php_fastcgi? reverse_proxy forwards HTTP requests to a backend server (Node.js, Go, Python). php_fastcgi communicates with PHP-FPM using the FastCGI protocol, specifically for PHP applications.

3. How do you reload Caddy configuration without downtime? Use sudo caddy reload --config /etc/caddy/Caddyfile. Caddy gracefully reloads the configuration while serving existing connections. Alternatively, use the Caddy API endpoint POST /load.

4. Challenge: Multi-service Caddy deployment

Configure Caddy to serve three services under one domain:

  • dodatech.com/ serves static files from /var/www/main
  • dodatech.com/api/* proxies to localhost:3000
  • dodatech.com/blog/* proxies to localhost:8080

Use path-based routing with Caddy's route directive.

Mini Project: Production Caddy Stack

Deploy Caddy as the primary web server for a multi-service architecture:

  1. Install Caddy on a Linux server
  2. Configure a Caddyfile with:
    • Static file server for www.dodatech.com with gzip and security headers
    • Reverse proxy for api.dodatech.com with load balancing across two Node.js instances
    • PHP-FPM for blog.dodatech.com running WordPress
  3. Verify automatic HTTPS certificates
  4. Configure JSON-format access logs to /var/log/caddy/
  5. Test the rate limiting by sending rapid requests
# Validate Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile

# Reload configuration
sudo caddy reload --config /etc/caddy/Caddyfile

# Test endpoints
curl -I https://www.dodatech.com/
curl https://api.dodatech.com/health
curl -I https://blog.dodatech.com/

This deployment pattern is used by DodaTech for internal microservices and Doda Browser telemetry endpoints that require quick, secure HTTPS.

FAQ

Is Caddy production-ready?

Yes. Caddy is used in production by thousands of organizations. It handles millions of requests per day, supports HTTP/2 and HTTP/3, and has a track record of stability since its initial release in 2015.

How does Caddy compare to NGINX?

Caddy's configuration is simpler and HTTPS is automatic. NGINX has more modules, wider ecosystem support, and higher raw performance for static files. Choose Caddy for ease of use and automatic TLS; choose NGINX for maximum flexibility.

Can Caddy serve as a reverse proxy for WebSocket?

Yes. Caddy's reverse_proxy handles WebSocket connections transparently. No special configuration is needed. Caddy detects the Upgrade header and proxies the connection accordingly.

Does Caddy support HTTP/3?

Yes. Caddy supports HTTP/3 (QUIC) out of the box. Enable it by adding the servers directive with protocol configuration, or let Caddy auto-detect and enable it.

How do I run Caddy behind another proxy?

Set the trusted_proxies directive to the IP range of your upstream proxy so Caddy logs the real client IP. Add servers { trusted_proxies static 10.0.0.0/8 } to your global options.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro