Caddy: reverse proxy с автоматическим HTTPS

caddyweb-serverhttpsreverse-proxy

Сетевая инфраструктура: кабели и коммутаторы

Nginx — это стандарт индустрии. Он быстрый, надёжный и проверен десятилетиями. Но каждый раз, когда я настраиваю на нём reverse proxy с HTTPS, я трачу время на одни и те же вещи: генерация сертификатов через certbot, настройка cron для обновления, правильные ssl-параметры, редирект с HTTP на HTTPS. Это рутина, которую можно автоматизировать.

Caddy делает именно это. Он автоматически получает и обновляет TLS-сертификаты от Let’s Encrypt, настраивает HTTPS по умолчанию и при этом имеет конфигурацию, которая читается как обычный текст. Я использую Caddy на нескольких проектах как основной reverse proxy и расскажу, почему он заслуживает внимания.

Почему Caddy

Три главных преимущества Caddy перед классическими web-серверами:

Автоматический HTTPS. Caddy получает сертификаты от Let’s Encrypt (или ZeroSSL) автоматически. Не нужен certbot, не нужны cron-задачи, не нужно думать об обновлении сертификатов. Caddy сам проходит ACME-challenge, получает сертификат и обновляет его до истечения срока.

Простая конфигурация. Caddyfile — это минималистичный формат, где типичный reverse proxy описывается в 3-4 строки. Никаких proxy_pass, proxy_set_header, ssl_certificate — Caddy делает всё это автоматически.

Безопасность по умолчанию. Caddy включает HTTPS для всех сайтов, использует современные TLS-настройки (TLS 1.3, OCSP stapling, HSTS), автоматически редиректит HTTP на HTTPS. Не нужно копировать сниппеты из Mozilla SSL Configuration Generator.

Установка

На Ubuntu/Debian:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
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

Или через Docker (мой предпочтительный способ):

docker pull caddy:2-alpine

После установки Caddy сразу готов к работе. По умолчанию он обслуживает файлы из текущей директории — можно проверить, что он запускается:

caddy run

Caddyfile: основы синтаксиса

Caddyfile — это конфигурационный файл Caddy. Его синтаксис построен вокруг блоков сайтов:

example.com {
    respond "Hello, World!"
}

Это всё, что нужно, чтобы запустить сайт с HTTPS. Caddy сам получит сертификат для example.com, настроит TLS и начнёт отвечать на запросы. Для сравнения, в Nginx это потребовало бы минимум 15-20 строк конфигурации плюс настройку certbot.

Множественные сайты

api.example.com {
    reverse_proxy localhost:8080
}

app.example.com {
    reverse_proxy localhost:3000
}

static.example.com {
    root * /var/www/static
    file_server
}

Три сайта, три домена, три TLS-сертификата — и никакой ручной работы с сертификатами.

Глобальные опции

Глобальные параметры задаются в блоке без адреса в начале файла:

{
    email admin@example.com
    acme_ca https://acme-v02.api.letsencrypt.org/directory
    log {
        output file /var/log/caddy/access.log
        format json
    }
}

email используется для регистрации в Let’s Encrypt и для уведомлений об истечении сертификатов.

Reverse Proxy

Основная задача Caddy в моих проектах — reverse proxy для бэкенд-сервисов. Базовая настройка:

api.example.com {
    reverse_proxy localhost:8080
}

Caddy автоматически проксирует заголовки X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host. Не нужно прописывать proxy_set_header как в Nginx.

Проксирование WebSocket

ws.example.com {
    reverse_proxy localhost:8080
}

Caddy автоматически определяет WebSocket-соединения по заголовку Upgrade и проксирует их. Никакой дополнительной настройки.

Health checks

api.example.com {
    reverse_proxy localhost:8080 {
        health_uri /health
        health_interval 10s
        health_timeout 5s
        health_status 200
    }
}

Caddy периодически проверяет доступность бэкенда и перестаёт отправлять на него запросы, если он не отвечает.

Load Balancing

Caddy поддерживает балансировку нагрузки между несколькими бэкендами:

api.example.com {
    reverse_proxy localhost:8081 localhost:8082 localhost:8083 {
        lb_policy round_robin
        health_uri /health
        health_interval 15s
    }
}

Доступные политики балансировки:

  • round_robin — по очереди (по умолчанию)
  • least_conn — на бэкенд с наименьшим числом активных соединений
  • random — случайный выбор
  • first — на первый доступный
  • ip_hash — привязка клиента к бэкенду по IP
  • uri_hash — привязка по URI
  • header — по значению заголовка
  • cookie — sticky sessions через cookie

Пример sticky sessions:

app.example.com {
    reverse_proxy localhost:3001 localhost:3002 {
        lb_policy cookie my_sticky_cookie
        health_uri /health
        health_interval 10s
    }
}

Headers и Middleware

Caddy использует систему middleware (называемых directives), которые обрабатывают запросы последовательно.

Управление заголовками

api.example.com {
    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
        -Server
    }
    reverse_proxy localhost:8080
}

Префикс - удаляет заголовок. Здесь мы убираем заголовок Server, чтобы не раскрывать информацию о web-сервере.

CORS

api.example.com {
    @cors_preflight method OPTIONS
    handle @cors_preflight {
        header Access-Control-Allow-Origin "https://app.example.com"
        header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
        header Access-Control-Allow-Headers "Content-Type, Authorization"
        header Access-Control-Max-Age "3600"
        respond "" 204
    }

    header Access-Control-Allow-Origin "https://app.example.com"
    reverse_proxy localhost:8080
}

Сжатие

app.example.com {
    encode gzip zstd
    reverse_proxy localhost:3000
}

Caddy поддерживает gzip и zstd, автоматически выбирая лучший вариант на основе заголовка Accept-Encoding клиента.

File Server

Caddy отлично подходит для раздачи статических файлов:

static.example.com {
    root * /var/www/static
    file_server {
        hide .git .env
        precompressed gzip br
    }
}

hide — скрывает указанные файлы и директории. precompressed — ищет заранее сжатые версии файлов (например, index.html.gz).

SPA (Single Page Application)

app.example.com {
    root * /var/www/app
    try_files {path} /index.html
    file_server
}

try_files — аналог try_files в Nginx. Если файл не найден — отдаёт index.html, что необходимо для SPA с клиентским роутингом.

Сравнение с Nginx

Терминал с конфигурацией Caddy

Вот один и тот же сценарий — reverse proxy с HTTPS — в Caddy и Nginx:

Caddy (3 строки):

api.example.com {
    reverse_proxy localhost:8080
}

Nginx (около 30 строк):

server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Плюс отдельно настроенный certbot и cron для обновления сертификатов. Разница в сложности очевидна.

При этом важно понимать: Nginx остаётся лучшим выбором для высоконагруженных систем, где нужна тонкая настройка каждого параметра. Caddy оптимален для проектов, где важнее скорость настройки и простота обслуживания.

Docker-деплой

Мой стандартный docker-compose.yml для Caddy:

services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3 (QUIC)
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
      - /var/www:/var/www:ro
    networks:
      - web

  api:
    image: myapi:latest
    container_name: api
    restart: unless-stopped
    expose:
      - "8080"
    networks:
      - web

  frontend:
    image: myfrontend:latest
    container_name: frontend
    restart: unless-stopped
    expose:
      - "3000"
    networks:
      - web

volumes:
  caddy-data:
  caddy-config:

networks:
  web:
    driver: bridge

Важные моменты:

  • Порт 443/udp нужен для HTTP/3 (QUIC) — Caddy поддерживает его из коробки
  • Volume caddy-data хранит TLS-сертификаты — не удаляйте его, иначе Caddy запросит сертификаты заново (Let’s Encrypt имеет rate limits)
  • Бэкенд-сервисы используют expose вместо ports — они доступны только внутри Docker-сети

Caddyfile для Docker-сетей:

api.example.com {
    reverse_proxy api:8080
}

app.example.com {
    reverse_proxy frontend:3000
}

В Docker-сетях можно обращаться к контейнерам по имени сервиса. Caddy резолвит DNS автоматически.

Caddy API: динамическая конфигурация

Одна из уникальных возможностей Caddy — полноценный REST API для управления конфигурацией без перезагрузки. Caddyfile на самом деле конвертируется в JSON-конфигурацию, и API работает именно с ней.

# Получить текущую конфигурацию
curl http://localhost:2019/config/

# Загрузить новую конфигурацию
curl -X POST http://localhost:2019/load 
  -H "Content-Type: application/json" 
  -d @caddy-config.json

# Добавить новый reverse proxy на лету
curl -X POST http://localhost:2019/config/apps/http/servers/srv0/routes 
  -H "Content-Type: application/json" 
  -d '{
    "match": [{"host": ["new-service.example.com"]}],
    "handle": [{
      "handler": "reverse_proxy",
      "upstreams": [{"dial": "localhost:9090"}]
    }]
  }'

API работает на порту 2019 (по умолчанию только localhost). Это позволяет интегрировать Caddy с CI/CD: деплой нового сервиса автоматически добавляет маршрут в Caddy без перезагрузки.

Для Caddyfile тоже есть hot reload без API:

caddy reload --config /etc/caddy/Caddyfile

Или в Docker:

docker exec caddy caddy reload --config /etc/caddy/Caddyfile

Basic Auth

Caddy имеет встроенную поддержку basic authentication:

# Генерация хеша пароля
caddy hash-password --plaintext 'my-secure-password'

Это вернёт bcrypt-хеш. Используйте его в Caddyfile:

admin.example.com {
    basicauth {
        admin $2a$14$...  # хеш от caddy hash-password
    }
    reverse_proxy localhost:8080
}

Можно ограничить auth для определённых путей:

app.example.com {
    basicauth /admin/* {
        admin $2a$14$...
    }
    reverse_proxy localhost:3000
}

Rate Limiting

Для rate limiting в Caddy нужен модуль caddy-ratelimit. Его можно добавить через кастомный билд:

# Установка xcaddy (инструмент для сборки Caddy с плагинами)
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# Сборка Caddy с модулем rate limit
xcaddy build --with github.com/mholt/caddy-ratelimit

Или через Docker с кастомным Dockerfile:

FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/mholt/caddy-ratelimit

FROM caddy:2-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

Использование в Caddyfile:

api.example.com {
    rate_limit {
        zone dynamic_zone {
            key {remote_host}
            events 100
            window 1m
        }
    }
    reverse_proxy localhost:8080
}

Это ограничивает каждый IP-адрес до 100 запросов в минуту. При превышении Caddy возвращает 429 Too Many Requests.

Полный пример: production Caddyfile

Вот Caddyfile, который я использую на одном из проектов:

{
    email admin@example.com
    log {
        output file /var/log/caddy/access.log {
            roll_size 100mb
            roll_keep 5
        }
        format json
    }
}

# API с rate limiting и CORS
api.example.com {
    log {
        output file /var/log/caddy/api.log {
            roll_size 50mb
            roll_keep 3
        }
    }

    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        -Server
    }

    reverse_proxy localhost:8080 {
        health_uri /health
        health_interval 15s
        header_up X-Real-IP {remote_host}
    }
}

# Frontend (SPA)
app.example.com {
    encode gzip zstd

    root * /var/www/app
    try_files {path} /index.html
    file_server {
        hide .git .env
    }
}

# Grafana
grafana.example.com {
    reverse_proxy localhost:3000
}

# Админка с basic auth
admin.example.com {
    basicauth {
        admin $2a$14$sOm3hAsH3dPaSsWoRd
    }
    reverse_proxy localhost:8081
}

# Редирект www на non-www
www.example.com {
    redir https://example.com{uri} permanent
}

# Основной сайт
example.com {
    encode gzip zstd
    root * /var/www/site
    file_server
}

Подводные камни и советы

DNS должен быть настроен до запуска. Caddy пытается получить сертификат сразу при старте. Если DNS A-запись не указывает на ваш сервер — ACME-challenge провалится. Убедитесь, что DNS настроен до запуска Caddy.

Rate limits Let’s Encrypt. Let’s Encrypt позволяет выпустить до 50 сертификатов на домен в неделю. Если вы часто пересоздаёте контейнер и удаляете volume caddy-data — можете упереться в лимит. Никогда не удаляйте этот volume без необходимости.

Порядок директив. В отличие от Nginx, порядок директив в Caddyfile не всегда соответствует порядку выполнения. Caddy использует предопределённый порядок обработки. Если нужен конкретный порядок — используйте route:

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

Wildcard-сертификаты. Для wildcard (*.example.com) Caddy требует DNS-challenge. Поддерживаются Cloudflare, Route53 и другие провайдеры через плагины:

*.example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    reverse_proxy localhost:8080
}

Логирование. По умолчанию Caddy пишет логи в stdout. Для production настройте вывод в файл с ротацией, как показано в примере выше.

Когда Caddy — правильный выбор

Caddy идеально подходит для:

  • Self-hosted проектов — поднять сервис с HTTPS за 5 минут
  • Разработки — автоматический HTTPS даже для localhost (через локальный CA)
  • Небольших production-сетапов — до десятка сервисов за одним reverse proxy
  • Прототипов и MVP — минимум конфигурации, максимум результата

Для высоконагруженных проектов с тысячами RPS, где нужна тонкая настройка worker-процессов, буферизации и кеширования — Nginx остаётся лучшим выбором. Но для 90% задач, с которыми сталкивается DevOps-инженер в повседневной работе, Caddy экономит время и снижает вероятность ошибки в конфигурации. А автоматический HTTPS — это та вещь, к которой привыкаешь мгновенно и не хочешь возвращаться обратно.

© 2026 Terminal Notes. Built with SvelteKit.