Caddy vs Nginx: когда современный веб-сервер побеждает классику

Исходная ситуация

Стартап «СтартВеб» разрабатывает SaaS-платформу и выпускает по 2-3 новых микросервиса в месяц. Каждый сервис нуждается в HTTPS, reverse proxy и иногда — статической раздаче файлов. Команда из 4 разработчиков и 1 DevOps-инженера использовала Nginx, но сталкивалась с повторяющимися проблемами:

  • Let's Encrypt — для каждого нового домена нужно настраивать certbot, cron для автопродления, следить за истечением сертификатов. Дважды за квартал сертификаты истекали из-за сбоя cron.
  • Конфигурация — типовая настройка reverse proxy в Nginx занимает 25-35 строк (server block, ssl параметры, proxy_pass, headers, location). При 15 сервисах это 450+ строк конфигурации, в которой легко ошибиться.
  • Перезагрузка — после изменения конфигурации нужен nginx -t && systemctl reload nginx. Забытый reload — сервис недоступен, забытый -t — Nginx падает.
  • Docker — при запуске нового контейнера нужно вручную добавить upstream и перезагрузить Nginx. Автоматизация через nginx-proxy работала нестабильно.

DevOps-инженер Дмитрий предложил попробовать Caddy — современный веб-сервер с автоматическим HTTPS. Мы провели пилот на одном проекте и через неделю перевели всю инфраструктуру.

Автоматический HTTPS: главная суперсила Caddy

Caddy автоматически получает и продлевает TLS-сертификаты от Let's Encrypt (или ZeroSSL). Не нужен certbot, cron, или ручная настройка. Достаточно указать доменное имя в конфигурации.

Сравните конфигурацию reverse proxy для одного сервиса:

Nginx (28 строк):

server {
    listen 80;
    server_name api.startweb.ru;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.startweb.ru;

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

    location / {
        proxy_pass http://127.0.0.1:3000;
        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";
    }
}

Caddy (3 строки):

api.startweb.ru {
    reverse_proxy localhost:3000
}

Три строки вместо 28. Caddy автоматически:

  • Получает TLS-сертификат от Let's Encrypt
  • Настраивает HTTPS с TLS 1.2/1.3 и современными cipher suites
  • Создаёт HTTP → HTTPS redirect
  • Включает HTTP/2 и HTTP/3 (QUIC)
  • Проксирует заголовки X-Forwarded-For, X-Forwarded-Proto
  • Продлевает сертификат автоматически за 30 дней до истечения

Caddyfile: конфигурация всей инфраструктуры

Вот полный Caddyfile для инфраструктуры «СтартВеб» из 8 сервисов:

# Глобальные настройки
{
    email devops@startweb.ru
    acme_ca https://acme-v02.api.letsencrypt.org/directory
    admin off  # отключаем admin API на production
    log {
        output file /var/log/caddy/access.log {
            roll_size 100MiB
            roll_keep 10
        }
    }
}

# Основной сайт
startweb.ru {
    root * /var/www/startweb
    file_server
    encode gzip zstd

    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    }
}

# API Backend (Go)
api.startweb.ru {
    reverse_proxy localhost:3000 {
        health_uri /health
        health_interval 10s
        health_timeout 3s
    }
    log {
        output file /var/log/caddy/api.log
    }
}

# Dashboard (React SPA)
dash.startweb.ru {
    reverse_proxy localhost:3001
    handle_path /api/* {
        reverse_proxy localhost:3000
    }
}

# WebSocket сервис
ws.startweb.ru {
    reverse_proxy localhost:3002
}

# Сервис с load balancing (3 инстанса)
app.startweb.ru {
    reverse_proxy localhost:4001 localhost:4002 localhost:4003 {
        lb_policy round_robin
        health_uri /healthz
        health_interval 15s
        fail_duration 30s
    }
}

# Grafana
mon.startweb.ru {
    reverse_proxy localhost:3100
}

# MinIO Console
s3.startweb.ru {
    reverse_proxy localhost:9001
}

# Staging (wildcard + basic auth)
*.staging.startweb.ru {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    basic_auth {
        dev $2a$14$hashhere
    }
    reverse_proxy localhost:5000
}

50 строк конфигурации для 8 сервисов — вместо ~250 строк в Nginx. Wildcard-сертификат для staging получается автоматически через DNS challenge (Cloudflare API). Для этого Caddy собран с модулем caddy-dns/cloudflare.

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

Caddy имеет REST API для управления конфигурацией без перезагрузки. Это позволяет динамически добавлять и удалять маршруты:

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

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

# Удалить маршрут по индексу
curl -X DELETE http://localhost:2019/config/apps/http/servers/srv0/routes/5

# Перезагрузить конфигурацию из файла
curl -X POST http://localhost:2019/load \
  -H "Content-Type: text/caddyfile" \
  --data-binary @/etc/caddy/Caddyfile

Мы написали простой скрипт для CI/CD, который при деплое нового сервиса автоматически регистрирует маршрут в Caddy:

#!/bin/bash
# deploy-service.sh — автоматическая регистрация в Caddy
SERVICE_NAME=$1
SERVICE_PORT=$2
DOMAIN="${SERVICE_NAME}.startweb.ru"

# Деплоим контейнер
docker run -d --name "$SERVICE_NAME" \
  --network app-network \
  -p "127.0.0.1:${SERVICE_PORT}:8080" \
  "registry.startweb.ru/${SERVICE_NAME}:latest"

# Добавляем маршрут в Caddy (Caddyfile approach)
cat >> /etc/caddy/Caddyfile << EOF

${DOMAIN} {
    reverse_proxy localhost:${SERVICE_PORT}
}
EOF

# Валидируем и применяем
caddy validate --config /etc/caddy/Caddyfile && \
  caddy reload --config /etc/caddy/Caddyfile

echo "Service ${SERVICE_NAME} deployed at https://${DOMAIN}"

Caddy получит сертификат для нового домена автоматически — обычно за 5-15 секунд после первого запроса.

Caddy с PHP-FPM и file_server

Для legacy-проекта на PHP (WordPress admin panel) мы настроили Caddy как замену Nginx + PHP-FPM:

# WordPress на Caddy
wp.startweb.ru {
    root * /var/www/wordpress

    # PHP-FPM через Unix socket
    php_fastcgi unix//run/php/php8.3-fpm.sock {
        index index.php
        resolve_root_symlink
    }

    file_server
    encode gzip

    # Запрет доступа к чувствительным файлам
    @blocked path /wp-config.php /xmlrpc.php /.htaccess
    respond @blocked 403

    # Кэширование статики
    @static path *.css *.js *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff2
    header @static Cache-Control "public, max-age=31536000, immutable"

    # Rate limiting на wp-login.php
    @wplogin path /wp-login.php
    rate_limit @wplogin {
        zone wplogin {
            key {remote_host}
            events 5
            window 60s
        }
    }
}

Для раздачи статических файлов (документация, SPA-приложения) Caddy предлагает встроенный file_server с browse (листинг директорий), сжатием и кэшированием:

# Документация с browse
docs.startweb.ru {
    root * /var/www/docs
    file_server browse
    encode gzip zstd

    # SPA fallback — все маршруты ведут на index.html
    try_files {path} /index.html
}

Сравнение производительности

Мы провели бенчмарк с помощью wrk на идентичном железе (4 vCPU, 8 GB RAM, Ubuntu 24.04):

Тест 1: Статическая раздача (файл 10 KB)

# wrk -t4 -c200 -d30s https://test-domain/static/10kb.html

# Nginx 1.26
Requests/sec: 48,234
Transfer/sec: 462 MB
Latency (avg): 4.1 ms

# Caddy 2.8
Requests/sec: 41,872
Transfer/sec: 401 MB
Latency (avg): 4.7 ms

Тест 2: Reverse proxy (backend Go, 1 ms response)

# wrk -t4 -c200 -d30s https://test-domain/api/health

# Nginx 1.26
Requests/sec: 35,120
Latency (avg): 5.6 ms
Latency (p99): 12.3 ms

# Caddy 2.8
Requests/sec: 33,450
Latency (avg): 5.9 ms
Latency (p99): 13.1 ms

Тест 3: TLS handshake (новые подключения)

# h2load -n 10000 -c 100 -m 1 https://test-domain/

# Nginx 1.26
TLS handshakes/sec: 2,340

# Caddy 2.8
TLS handshakes/sec: 2,180

Caddy на 10-15% медленнее Nginx в синтетических тестах. На практике разница незаметна: при 1000 RPS (типичная нагрузка СтартВеб) обе системы справляются без проблем, и узким местом является бэкенд, а не веб-сервер.

Потребление памяти:

МетрикаNginxCaddy
RSS (idle, 15 sites)12 MB45 MB
RSS (1000 RPS)28 MB85 MB
RSS (10000 RPS)45 MB140 MB

Caddy потребляет больше памяти (Go runtime, встроенный ACME клиент, OCSP stapling cache), но для современных серверов 140 MB — несущественно.

Docker-интеграция и модули

Caddy отлично работает в Docker-окружении. Наш docker-compose для типового проекта:

# docker-compose.yml
version: '3.8'

services:
  caddy:
    image: caddy:2.8-alpine
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3 (QUIC)
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    environment:
      - CF_API_TOKEN=${CF_API_TOKEN}
    restart: unless-stopped
    networks:
      - app-network

  api:
    image: registry.startweb.ru/api:latest
    expose:
      - "3000"
    restart: unless-stopped
    networks:
      - app-network

  frontend:
    image: registry.startweb.ru/frontend:latest
    expose:
      - "3001"
    restart: unless-stopped
    networks:
      - app-network

volumes:
  caddy_data:
  caddy_config:

networks:
  app-network:

Caddyfile для Docker-сетей использует имена контейнеров:

api.startweb.ru {
    reverse_proxy api:3000
}

app.startweb.ru {
    reverse_proxy frontend:3001
}

Для модулей (DNS challenge, rate limiting, cache) нужна пересборка Caddy с xcaddy:

# Dockerfile для кастомного Caddy
FROM caddy:2.8-builder AS builder

RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare \
    --with github.com/mholt/caddy-ratelimit \
    --with github.com/caddyserver/cache-handler

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

Когда Caddy побеждает и когда лучше Nginx

После 6 месяцев эксплуатации Caddy на production мы сформулировали чёткие критерии выбора.

Выбирайте Caddy, если:

  • Команда маленькая (1-5 человек) и время DevOps-инженера дороже 15% производительности
  • Нужен автоматический HTTPS без боли с certbot и cron
  • Много доменов/сервисов — каждый новый сервис требует 3 строки вместо 30
  • Rapid prototyping — новый сервис в production за 5 минут
  • HTTP/3 (QUIC) из коробки без дополнительных танцев
  • API для динамического управления конфигурацией

Выбирайте Nginx, если:

  • High-load (50 000+ RPS) — Nginx эффективнее использует ресурсы
  • Нужен тонкий тюнинг (worker_connections, proxy_buffer_size, limit_req с зонами)
  • Legacy-инфраструктура с существующей конфигурацией Nginx
  • Модули, которых нет в Caddy (njs, Lua/OpenResty, GeoIP2)
  • Требуется сертификация или compliance (Nginx используется в PCI DSS скоупе годами)

Результаты для «СтартВеб»:

МетрикаС NginxС Caddy
Время добавления нового сервиса20-30 минут2 минуты
Инциденты с сертификатами/квартал20
Строк конфигурации (15 сервисов)~450~75
Время на поддержку веб-сервера/месяц8 часов1 час

Часто задаваемые вопросы

Да, Caddy лицензирован под Apache 2.0 и полностью бесплатен для коммерческого использования без ограничений. Компания ZeroSSL (создатель Caddy) зарабатывает на коммерческих сертификатах и enterprise-поддержке, а не на лицензировании самого веб-сервера.
Caddy попробует получить сертификат от ZeroSSL как fallback CA. Если оба недоступны — Caddy будет обслуживать запросы по HTTP и повторять попытки получения сертификата каждые несколько минут. Логи покажут ошибку с деталями (DNS не резолвится, порт 80 закрыт, rate limit). Существующие сертификаты продолжат работать до истечения.
Можно, но это не его сильная сторона. Для Kubernetes лучше использовать Nginx Ingress Controller, Traefik или Envoy. Caddy оптимален как edge proxy для VM-based инфраструктуры, Docker Compose и небольших кластеров. Для K8s существует caddy-ingress-controller, но его экосистема значительно уступает Nginx Ingress.
Да, reverse_proxy в Caddy прозрачно проксирует WebSocket-подключения без дополнительной конфигурации (Connection: upgrade обрабатывается автоматически). gRPC поддерживается через reverse_proxy с HTTP/2 бэкендом: reverse_proxy h2c://localhost:50051. Для gRPC-web используйте модуль caddy-grpc-web.
Параллельный запуск: установите Caddy на порту 8443, перенесите конфигурацию, протестируйте каждый сервис. Затем измените DNS TTL на 60 секунд, переключите Caddy на порты 80/443 и остановите Nginx. Весь процесс занимает 1-2 часа для 10-15 сервисов. Откат — запуск Nginx обратно за 30 секунд.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#caddy#nginx#web server#https#lets encrypt#reverse proxy#caddyfile#load balancing
Комментарии 0

Оставить комментарий

загрузка...