Caddy: The Modern Web Server That Makes HTTPS Easy
devops

Caddy: The Modern Web Server That Makes HTTPS Easy

Kaitou
Kaitou

What is Caddy?

Caddy is a powerful, enterprise-ready, open-source web server with automatic HTTPS written in Go. Unlike traditional web servers like Apache or Nginx, Caddy is designed with simplicity and security in mind, making it incredibly easy to deploy modern web applications.

Reference: https://caddyserver.com/

Why Choose Caddy?

  • Automatic HTTPS
  • Simple Setup
  • HTTP/2 & HTTP/3

Automatic HTTPS

One of Caddy's standout features is automatic HTTPS. It automatically obtains and renews TLS certificates from Let's Encrypt without any configuration. This means your sites are secure by default!

HTTPS Certificate Flow:
Client ──➤ Caddy ──➤ Let's Encrypt ──➤ Certificate ──➤ Automatic Renewal

Zero Configuration

Caddy works out of the box with sensible defaults. You can serve a static website with just one command, no configuration files needed.

Simple Configuration

When you do need configuration, Caddy uses a simple, human-readable format called the Caddyfile. No complex syntax or cryptic directives.

# Apache Configuration (Complex)
- <VirtualHost *:80>
-     ServerName caddy.localhost
-     DocumentRoot /var/www/html
-     <Directory /var/www/html>
-         AllowOverride All
-         Require all granted
-     </Directory>
- </VirtualHost>

# Caddy Configuration (Simple)
+ caddy.localhost
+ root * /var/www/html
+ file_server

Modern Features

  • HTTP/2 and HTTP/3 support
  • Automatic compression
  • Load balancing
  • Reverse proxy capabilities
  • API endpoints
  • Plugin ecosystem
HTTP Evolution Timeline:
HTTP/1.1 (1997) ──➤ HTTP/2 (2015) ──➤ HTTP/3 (2022)
   Single           Multiplexing       QUIC Protocol
   Connection       Streams            UDP Based

Key Concepts

1. Caddyfile

The Caddyfile is Caddy's primary configuration format. It's designed to be easy to read and write:

caddy.localhost

root * /var/www
file_server

2. Directives

Directives tell Caddy what to do. Common directives include:

  • file_server: Serve static files
  • reverse_proxy: Proxy requests to backend servers
  • respond: Send custom responses
  • rewrite: Modify requests

3. Matchers

Matchers allow you to apply directives conditionally:

  • Path matchers: /api/*
  • Header matchers: {header.user-agent}*curl*
  • Method matchers: {method.POST}
Request Processing Flow:
Request → Matchers → Handlers → Response

Getting Started: Setting Up a Simple App

Let's build a simple web application setup with Caddy that serves both static files and a Node.js API.

Application Architecture:
Browser ↔ Caddy ↔ Node.js API
           ↓
      Static Files

Project Structure

Let's create a simple project structure:

simple-app/
├── Caddyfile                 
├── docker-compose.yml                 
├── public/                  
│   ├── index.html          
└── api/                    
    ├── package.json        
    └── server.js           

1. create docker-compose.yml

services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
      - ./public:/var/www/html
    networks:
      - caddy_net
  api:
    image: node:18
    container_name: node_api
    restart: unless-stopped
    working_dir: /usr/src/app
    volumes:
      - ./api:/usr/src/app
    command: sh -c "npm install && npm start"
    ports:
      - "3000:3000"
    networks:
      - caddy_net    
volumes:
  caddy_data:
  caddy_config:
networks:
  caddy_net:
    driver: bridge

1. Create the Frontend

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple Caddy App</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1> Welcome to Caddy!</h1>
        <p>This is a simple app served by Caddy web server.</p>
        
        <div class="api-demo">
            <h2>API Demo</h2>
            <button id="fetch-data">Fetch Data from API</button>
            <div id="result"></div>
        </div>
        
        <div class="features">
            <h2>Caddy Features</h2>
            <ul>
                <li>Automatic HTTPS</li>
                <li>HTTP/2 & HTTP/3</li>
                <li>Zero Configuration</li>
                <li>Hot Reload</li>
                <li>Reverse Proxy</li>
            </ul>
        </div>
    </div>
    
    <script>
        document.getElementById('fetch-data').addEventListener('click', async () => {
            const resultDiv = document.getElementById('result');
            resultDiv.textContent = 'Loading...';
            try {
                const response = await fetch('/api/data');
                const data = await response.json();
                resultDiv.textContent = `Message from API: ${data.message}`;
            } catch (error) {
                resultDiv.textContent = 'Error fetching data from API.';
            }
        });
    </script>
</body>
</html>

2. Create the Backend API

api/package.json

{
  "name": "simple-api",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "cors": "^2.8.5"
  }
}

api/server.js

const express = require('express');
const cors = require('cors');

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(express.json());

// Routes
app.get('/api/data', (req, res) => {
    const data = {
        message: 'Hello from Caddy-proxied API!',
        timestamp: new Date().toISOString(),
        server: 'Node.js + Express',
        features: [
            'Automatic HTTPS',
            'Reverse Proxy',
            'Load Balancing',
            'Hot Reload'
        ],
        stats: {
            uptime: process.uptime(),
            memory: process.memoryUsage(),
            version: process.version
        }
    };
    
    res.json(data);
});

app.get('/api/health', (req, res) => {
    res.json({ 
        status: 'healthy',
        timestamp: new Date().toISOString()
    });
});

app.listen(PORT, () => {
    console.log(`API server running on http://localhost:${PORT}`);
    console.log(`Health check: http://localhost:${PORT}/api/health`);
});

3. Configure Caddy

Caddyfile

caddy.localhost {
	# Reverse Proxy to Node.js API
	reverse_proxy /api/* api:3000

	# Serve static files for everything else
	root * /var/www/html
	file_server

	handle /health {
		respond "OK" 200
	}

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

4. Running the Application

docker compose up -d

Additional Resources