Skip to main content
Back to blog

Caddy as a reverse proxy for self-hosted services

·3 min readNetworking

I switched from Nginx to Caddy a couple of years ago and it remains one of the best infrastructure decisions I have made. I wrote about the switch itself in a previous post, but this one goes deeper into how I use Caddy day to day and the features that make it worth recommending.

Automatic HTTPS

This is the main selling point and it is not overstated. Caddy obtains and renews TLS certificates automatically using Let's Encrypt. You do not configure anything. Point a domain at your server, add it to the Caddyfile, and HTTPS just works.

No Certbot. No cron jobs. No manual renewal. No expired certificate emergencies at 2 AM.

The Caddyfile

Caddy's configuration is a file called Caddyfile. Here is a reverse proxy for three services:

nextcloud.example.com {
    reverse_proxy localhost:8080
}

gitea.example.com {
    reverse_proxy localhost:3000
}

monitoring.example.com {
    reverse_proxy localhost:9090
}

That is the entire config. Each block is a domain name with a reverse proxy directive. Caddy handles HTTPS, HTTP to HTTPS redirects, and header management automatically.

Compare that to the equivalent Nginx config with SSL blocks, certificate paths, redirect rules, and proxy headers. The Caddyfile is dramatically simpler.

Running with Docker Compose

I run Caddy in Docker alongside my other services:

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
 
volumes:
  caddy_data:
  caddy_config:

The caddy_data volume stores your TLS certificates. Keep it persistent and you will not hit rate limits when recreating the container.

Headers and security

Caddy sets sensible security headers by default. If you need more control:

app.example.com {
    reverse_proxy localhost:3000
    header {
        X-Frame-Options "DENY"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

Basic authentication

Need to protect a service that does not have its own auth? Caddy has built-in basic auth:

admin.example.com {
    basicauth {
        admin $2a$14$hashed_password_here
    }
    reverse_proxy localhost:8080
}

Generate the hash with caddy hash-password.

When Nginx still makes sense

If you need advanced load balancing, complex routing rules, or you are already deep in the Nginx ecosystem with existing configs, switching has a cost. Caddy is also younger and has a smaller community, though it is growing fast.

For self-hosted services where you are adding and removing subdomains regularly, Caddy's simplicity wins. I migrated my entire setup in about 20 minutes and have not looked back.

Sources

Enjoying the blog? Subscribe via RSS to get new posts in your reader.

Subscribe via RSS