Traefik vs Caddy vs Nginx: сравнение Docker reverse proxy

10 мин чтения·Matthieu·docker-composelets-encryptreverse-proxynginxcaddytraefikdocker|

Три рабочих стека Docker Compose для Traefik, Caddy и Nginx в качестве reverse proxy на VPS. Один бэкенд, реальные бенчмарки и фреймворк для выбора подходящего решения.

На VPS крутятся Docker-контейнеры. Нужен HTTPS, маршрутизация по доменному имени и единая точка входа. Traefik, Caddy и Nginx решают эту задачу. Решают по-разному.

В этой статье три рабочих стека Docker Compose, которые разворачивают один и тот же бэкенд за каждым прокси. Копируй тот, который подходит под твою ситуацию. Таблица сравнения и фреймворк принятия решений в конце помогут определиться.

Все примеры используют выделенную сеть для прокси, редирект с HTTP на HTTPS и продакшн-настройки по умолчанию. Стеки рассчитаны на Ubuntu 24.04 на VPS Virtua Cloud.

Что делает reverse proxy для Docker-контейнеров на VPS?

Reverse proxy располагается между интернетом и Docker-контейнерами. Он терминирует TLS (HTTPS), маршрутизирует запросы к нужному контейнеру по имени хоста и выставляет одну пару портов (80/443) вместо отдельного порта для каждого сервиса. Контейнеры никогда не работают с сертификатами и не биндятся на публичные порты напрямую.

Без reverse proxy каждому контейнеру нужен свой публичный порт. Посетители заходили бы на example.com:3000 для одного сервиса и example.com:8080 для другого. Reverse proxy позволяет использовать app.example.com и api.example.com на стандартных портах.

Все три стека ниже предполагают:

  • Docker и Docker Compose установлены
  • DNS A-запись указывает на IP VPS
  • Порты 80 и 443 открыты в файрволе
  • Есть SSH-доступ от имени не-root пользователя с sudo

Каждый пример разворачивает один и тот же бэкенд: образ traefik/whoami, который возвращает HTTP-заголовки и информацию о контейнере. Замени его на своё приложение позже.

Как настроить Traefik в качестве Docker reverse proxy с автоматическим HTTPS?

Traefik обнаруживает контейнеры автоматически, читая Docker-лейблы. Правила маршрутизации добавляются как лейблы на каждый сервис. Когда контейнер стартует, Traefik обнаруживает его, запрашивает сертификат Let's Encrypt и начинает маршрутизировать трафик. Перезагрузка конфигурации не нужна.

Создай директорию проекта:

mkdir -p ~/traefik-proxy && cd ~/traefik-proxy

Создай Docker-сеть, которую будут использовать все проксируемые сервисы:

docker network create proxy

Создай docker-compose.yml:

services:
  traefik:
    image: traefik:v3.6
    container_name: traefik
    restart: unless-stopped
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=proxy"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--certificatesresolvers.letsencrypt.acme.email=you@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--log.level=WARN"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./acme.json:/acme.json
    networks:
      - proxy
    security_opt:
      - no-new-privileges:true

  whoami:
    image: traefik/whoami
    container_name: whoami
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`app.example.com`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
    networks:
      - proxy

networks:
  proxy:
    external: true

Перед запуском создай файл хранения сертификатов с ограниченными правами:

touch acme.json && chmod 600 acme.json

Traefik откажется стартовать, если у acme.json слишком открытые права. 600 гарантирует, что только владелец может читать хранящиеся внутри приватные ключи.

Запусти стек:

docker compose up -d

Проверь, что оба контейнера работают:

docker compose ps

И traefik, и whoami должны показывать статус Up. Проверь с локальной машины (не с сервера):

curl https://app.example.com

Ответ содержит вывод whoami с заголовками запроса. Заголовок X-Forwarded-For в ответе говорит о том, что Traefik проксирует трафик и терминирует TLS.

Что делают лейблы:

  • traefik.enable=true включает этот контейнер (так как установлено exposedbydefault=false)
  • traefik.http.routers.whoami.rule=Host(...) фильтрует запросы по имени хоста
  • traefik.http.routers.whoami.tls.certresolver=letsencrypt указывает Traefik получить сертификат для этого домена

Чтобы добавить ещё один сервис, добавь его в любой Compose-файл в той же сети proxy с нужными лейблами. Traefik подхватит его автоматически.

Безопасно ли монтировать Docker-сокет в Traefik?

Монтирование /var/run/docker.sock даёт Traefik полный доступ к Docker API. Если атакующий скомпрометирует Traefik, он сможет создавать контейнеры, читать переменные окружения (включая секреты) и эскалировать привилегии до root на хосте. Флаг :ro предотвращает только запись на уровне файловой системы. Он не ограничивает вызовы Docker API.

Для продакшна используй прокси для Docker-сокета. Он располагается между Traefik и Docker-демоном, фильтруя API-вызовы и разрешая только операции чтения метаданных контейнеров.

Добавь в docker-compose.yml:

services:
  socket-proxy:
    image: tecnativa/docker-socket-proxy:0.4
    container_name: socket-proxy
    restart: unless-stopped
    environment:
      CONTAINERS: 1
      NETWORKS: 1
      SERVICES: 0
      TASKS: 0
      POST: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - socket-proxy
    security_opt:
      - no-new-privileges:true

  traefik:
    image: traefik:v3.6
    container_name: traefik
    restart: unless-stopped
    depends_on:
      - socket-proxy
    command:
      - "--providers.docker.endpoint=tcp://socket-proxy:2375"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=proxy"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--certificatesresolvers.letsencrypt.acme.email=you@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--log.level=WARN"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./acme.json:/acme.json
    networks:
      - proxy
      - socket-proxy
    security_opt:
      - no-new-privileges:true
networks:
  proxy:
    external: true
  socket-proxy:
    driver: bridge
    internal: true

Traefik больше не монтирует Docker-сокет напрямую. Сеть socket-proxy помечена как internal: true, то есть у неё нет исходящего доступа в интернет. Прокси сокета разрешает только GET-запросы к эндпоинтам containers и networks.

Как настроить Caddy в качестве Docker reverse proxy с автоматическим HTTPS?

Caddy берёт на себя HTTPS автоматически, без настроек кроме доменного имени. Направь домен на сервер, укажи его в Caddyfile, и Caddy получит и обновит сертификаты от Let's Encrypt. Никаких резолверов, никаких настроек ACME. Это кратчайший путь к HTTPS для Docker reverse proxy.

Создай директорию проекта:

mkdir -p ~/caddy-proxy && cd ~/caddy-proxy

Создай общую сеть для прокси (пропусти, если уже создал для Traefik):

docker network create proxy

Создай Caddyfile:

app.example.com {
	reverse_proxy whoami:80
	encode gzip
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "DENY"
		Referrer-Policy "strict-origin-when-cross-origin"
	}
}

Это вся конфигурация прокси. Caddy читает доменное имя, запрашивает сертификат и проксирует на контейнер whoami на порту 80. Никаких резолверов сертификатов, никакого ACME-email (Caddy использует дефолтный с машины, или можно задать глобально), никаких путей хранения.

Создай docker-compose.yml:

services:
  caddy:
    image: caddy:2.11
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - proxy
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

  whoami:
    image: traefik/whoami
    container_name: whoami
    restart: unless-stopped
    networks:
      - proxy

networks:
  proxy:
    external: true

volumes:
  caddy_data:
  caddy_config:

Порт 443:443/udp включает HTTP/3 (QUIC), который Caddy поддерживает из коробки. cap_drop: ALL с cap_add: NET_BIND_SERVICE убирает все Linux capabilities кроме нужного для биндинга на порты ниже 1024.

Запусти стек:

docker compose up -d

Проверь статус контейнеров:

docker compose ps

Оба контейнера должны показывать Up. Проверь с локальной машины с подробным выводом:

curl -v https://app.example.com

Ищи HTTP/2 200 в выводе. Также должны быть видны заголовки безопасности из Caddyfile (Strict-Transport-Security, X-Content-Type-Options и т.д.).

Чтобы добавить ещё один сервис, добавь новый блок в Caddyfile с доменом и директивой reverse_proxy, затем перезагрузи:

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

Перезапуск контейнера не нужен. Caddy не нуждается в Docker-сокете. Он не обнаруживает контейнеры автоматически. Маршрутизация управляется в Caddyfile.

Как настроить Nginx в качестве Docker reverse proxy с Let's Encrypt?

Nginx даёт полный контроль над каждой директивой прокси, заголовком, размером буфера и правилом кэширования. Обратная сторона — ручная настройка. Nginx не получает TLS-сертификаты самостоятельно. Его объединяют с Certbot, который обрабатывает ACME-челленджи и обновление сертификатов.

Создай директорию проекта:

mkdir -p ~/nginx-proxy && cd ~/nginx-proxy

Создай общую сеть для прокси:

docker network create proxy

Создай конфигурацию Nginx в nginx/conf.d/app.conf:

mkdir -p nginx/conf.d
server {
    listen 80;
    server_name app.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    http2 on;
    server_name app.example.com;

    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers off;

    server_tokens off;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        proxy_pass http://whoami:80;
        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;
    }
}

server_tokens off; скрывает версию Nginx из заголовков ответа. Раскрытие версии помогает атакующим нацеливаться на известные уязвимости.

Создай docker-compose.yml:

services:
  nginx:
    image: nginx:1.28
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot_webroot:/var/www/certbot:ro
      - certbot_certs:/etc/letsencrypt:ro
    networks:
      - proxy
    depends_on:
      - whoami

  certbot:
    image: certbot/certbot
    container_name: certbot
    restart: unless-stopped
    volumes:
      - certbot_webroot:/var/www/certbot
      - certbot_certs:/etc/letsencrypt
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

  whoami:
    image: traefik/whoami
    container_name: whoami
    restart: unless-stopped
    networks:
      - proxy

networks:
  proxy:
    external: true

volumes:
  certbot_webroot:
  certbot_certs:

Nginx требует, чтобы файлы сертификатов существовали до запуска. Конфигурация выше ссылается на /etc/letsencrypt/live/app.example.com/fullchain.pem, которого ещё нет. Для первого сертификата временно замени app.conf на версию только с HTTP:

server {
    listen 80;
    server_name app.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

Запусти Nginx и бэкенд:

docker compose up -d nginx whoami

Запроси первый сертификат:

docker compose run --rm certbot certonly \
  --webroot \
  --webroot-path=/var/www/certbot \
  -d app.example.com \
  --email you@example.com \
  --agree-tos \
  --no-eff-email

После получения сертификата верни полный app.conf (версию с SSL-блоком, показанную выше) и запусти полный стек:

docker compose up -d

Проверь, что все контейнеры работают:

docker compose ps

Проверь с локальной машины:

curl -v https://app.example.com

Заголовок ответа server: должен показывать nginx без номера версии, что подтверждает работу server_tokens off.

Чтобы добавить ещё один сервис, создай новый .conf-файл в nginx/conf.d/ и перезагрузи:

docker compose exec nginx nginx -s reload

Для обновления сертификатов контейнер Certbot запускает certbot renew каждые 12 часов. После обновления перезагрузи Nginx, чтобы он подхватил новые сертификаты. Автоматизируй это с помощью cron-задачи или скрипта, проверяющего время изменения файлов сертификатов. Подробнее о настройке Nginx как reverse proxy читай в нашем руководстве.

Как соотносятся Traefik, Caddy и Nginx для Docker reverse proxying?

Traefik выигрывает в автообнаружении. Caddy выигрывает в простоте. Nginx выигрывает в контроле. Таблица ниже раскладывает компромиссы, которые важны при запуске Docker-контейнеров на VPS.

Характеристика Traefik v3 Caddy 2.11 Nginx 1.28
Автообнаружение Да (Docker-лейблы) Нет (ручной Caddyfile) Нет (ручные conf-файлы)
Автоматизация TLS Встроенный ACME Встроенный ACME Требует Certbot-sidecar
Способ конфигурации Docker-лейблы + статический YAML/CLI Caddyfile или JSON API Файлы nginx.conf
Перезагрузка конфига Автоматически по событиям контейнеров caddy reload (без даунтайма) nginx -s reload (без даунтайма)
Docker-сокет нужен Да (или прокси сокета) Нет Нет
HTTP/3 (QUIC) Экспериментально Да (по умолчанию) Через сторонний модуль
Middleware/плагины Встроенные (rate limit, auth, headers) Встроенные + Go-плагины Через директивы конфига
Сообщество/документация Большое, активное, хорошая дока Меньшее, отличная дока Самое большое, обширная дока
Кривая обучения Средняя (лейблы + статический конфиг) Низкая (Caddyfile интуитивен) Высокая (много директив)

Какой reverse proxy потребляет меньше всего памяти?

Потребление памяти в простое важно на VPS, где каждый мегабайт на счету. Эти цифры получены с помощью docker stats --no-stream на VPS Virtua Cloud с 4 vCPU / 8 ГБ RAM под Ubuntu 24.04. Каждый прокси работал в простое без трафика перед замером.

Прокси RAM в простое Размер образа
Traefik v3.6 ~17 МБ ~242 МБ
Caddy 2.11 ~14 МБ ~88 МБ
Nginx 1.28 ~5 МБ ~240 МБ
Nginx + Certbot ~5 МБ + ~25 МБ ~240 МБ + ~298 МБ

Nginx потребляет значительно меньше всех. Caddy посередине. Повышенное потребление Traefik связано с хранением в памяти состояния Docker-провайдера и таблицы маршрутизации. Все три используют дефолтные образы (на базе Debian/Alpine). Alpine-варианты уменьшат размер образов ценой возможных проблем совместимости с некоторыми расширениями.

Под лёгкой нагрузкой (100 параллельных запросов через wrk) все три справляются с трафиком без заметного роста CPU или памяти на этом размере VPS. Различия проявляются только при масштабировании или на самых маленьких тарифах VPS.

Как выбрать правильный reverse proxy для своей Docker-конфигурации?

Правильный выбор зависит от того, сколько сервисов ты запускаешь, как часто они меняются и что ты уже знаешь.

Выбирай Traefik, когда:

  1. Много контейнеров, которые часто меняются (добавление/удаление сервисов каждую неделю)
  2. Хочешь маршрутизацию без ручного вмешательства: задеплоил контейнер с лейблами — он в сети
  3. Используешь Docker Swarm или нужен service discovery на нескольких нодах
  4. Принимаешь доступ к Docker-сокету (с прокси сокета для продакшна)

Выбирай Caddy, когда:

  1. Немного сервисов, которые редко меняются
  2. Хочешь самый простой путь к автоматическому HTTPS
  3. Не хочешь монтировать Docker-сокет
  4. Ценишь маленький образ и низкое потребление памяти
  5. Хочешь HTTP/3 без дополнительной настройки

Выбирай Nginx, когда:

  1. Уже знаешь конфигурацию Nginx
  2. Нужен тонкий контроль над поведением прокси (буферы, кэширование, кастомные заголовки по location)
  3. Хочешь минимально возможное потребление памяти
  4. В команде уже есть инструменты и мониторинг для Nginx
  5. Не проблема управлять Certbot отдельно

Дерево решений:

  1. Больше 5 Docker-сервисов, которые регулярно меняются? Да -> Traefik
  2. Нужна тонкая настройка прокси или уже используешь Nginx? Да -> Nginx
  3. Хочешь минимум движущихся частей и самый быстрый старт? Да -> Caddy

Для большинства инди-хакеров, деплоящих один-два пет-проекта, Caddy — лучшая стартовая точка. Для DevOps-команд, управляющих флотом контейнеров, автообнаружение Traefik окупает себя. Для команд, уже использующих Nginx в других местах, оставаться на Nginx — значит держать стек единообразным.

Усиление безопасности для всех трёх прокси

Какой бы прокси ты ни выбрал, применяй эти базовые практики безопасности.

Заголовки безопасности. Все три примера выше включают HSTS, X-Content-Type-Options, X-Frame-Options и Referrer-Policy. Для Traefik добавляй их как middleware-лейблы:

labels:
  - "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
  - "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
  - "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
  - "traefik.http.middlewares.security-headers.headers.frameDeny=true"
  - "traefik.http.routers.whoami.middlewares=security-headers"

Rate limiting. У Traefik есть встроенный middleware для rate limiting. У Caddy есть директива rate_limit в виде плагина. Nginx использует limit_req_zone в конфигурации. Rate limiting защищает бэкенд от брутфорса и злоупотреблений.

Изоляция сети Docker. Каждый пример использует внешнюю сеть proxy. Бэкенд-сервисы не должны находиться в дефолтной bridge-сети. Только контейнеры, которым нужен прокси, присоединяются к сети proxy. Контейнеры с базами данных и внутренние сервисы остаются в отдельных внутренних сетях.

Файрвол. Только порты 80 и 443 должны быть доступны публично. Docker манипулирует iptables напрямую, что может обойти правила UFW. Решение описано в нашем руководстве.

Логи. Смотри логи прокси, когда что-то идёт не так:

# Traefik
docker logs traefik -f

# Caddy
docker logs caddy -f

# Nginx
docker logs nginx -f

Для Traefik временно установи --log.level=DEBUG для диагностики проблем с маршрутизацией или сертификатами. Для Caddy включи глобальную опцию debug в Caddyfile. Для Nginx проверь error.log внутри контейнера по пути /var/log/nginx/error.log.

Что-то пошло не так?

Симптом Вероятная причина Решение
Сертификат не выдан DNS A-запись не указывает на IP VPS Проверь с помощью dig app.example.com
Traefik 404 на всех маршрутах Контейнер не в сети proxy Проверь docker network inspect proxy
Caddy «permission denied» на порту 80 Отсутствует capability NET_BIND_SERVICE Добавь cap_add: NET_BIND_SERVICE
Nginx «no such file» для сертификата Certbot ещё не запускался Сначала запусти certbot certonly
ERR_CONNECTION_REFUSED Файрвол блокирует 80/443 Проверь ufw status или iptables -L
Ошибка прав acme.json в Traefik Слишком открытые права файла Запусти chmod 600 acme.json
Прокси работает на сервере, не работает снаружи Тестирование только на localhost Проверь curl с локальной машины

Для продакшн-усиления за пределами reverse proxying читай наше руководство по лимитам ресурсов и health checks в Compose-стеках.