Caddy как reverse proxy для Docker: compose с автоматическим HTTPS
Запускаем Caddy как reverse proxy для Docker: один compose-стек, общая сеть, reverse_proxy по именам сервисов и автоматический HTTPS, плюс когда помогают метки caddy-docker-proxy.
Коротко: запустите Caddy в отдельном контейнере, который публикует порты 80 и 443, подключите его и контейнеры приложений к одной общей Docker-сети и направьте `reverse_proxy` на имя каждого сервиса и его внутренний порт (например, `reverse_proxy app:3000`). Caddy разрешает имя через встроенный DNS Docker и автоматически получает TLS-сертификат для каждого адреса сайта.
В этом руководстве сначала показан подход с обычным Caddyfile, затем вариант caddy-docker-proxy на метках, и объясняется, когда какой подходит для одного VPS. Если вы запускаете Caddy прямо на хосте, а не в Docker, см. базовое руководство по Caddy как reverse proxy.
Как всё связано
Порты наружу публикует только контейнер Caddy. Каждый бэкенд-контейнер подключается к пользовательской bridge-сети (здесь — `web`) и открывает свой порт только в этой сети. Встроенный DNS-сервер Docker, доступный в пользовательских сетях, позволяет Caddy обращаться к `app:3000` по имени сервиса или контейнера, так что вам не нужно прописывать IP контейнеров, которые меняются при каждом пересоздании.
Не публиковать бэкенды важнее, чем кажется: Docker пишет собственные правила iptables, поэтому порт, опубликованный через `ports:`, может быть доступен из интернета, даже если ufw показывает, что он закрыт. Для всего, что должно стоять за прокси, используйте `expose:` или вообще не пробрасывайте порты.
| Компонент | Роль | Опубликован на хосте? |
|---|---|---|
| контейнер caddy | Терминация TLS, маршрутизация по имени хоста, редирект с HTTP на HTTPS | Да: 80/tcp, 443/tcp, 443/udp |
| сеть web | Общая bridge-сеть со встроенным DNS для имён сервисов | Нет |
| контейнеры приложений | Ваши сервисы (веб-приложения, API, дашборды) | Нет, доступны только через Caddy |
| том caddy_data | Сертификаты, приватные ключи, ACME-аккаунт | Нет, но должен сохраняться |
Пошагово: обычный Caddyfile с docker compose
- 01.До запуска Caddy направьте A-запись (и AAAA-запись, если используете IPv6) каждого имени хоста на VPS, иначе выпуск сертификата не удастся.
- 02.Один раз создайте общую сеть: `docker network create web`.
- 03.Создайте `/opt/proxy/compose.yaml` и `/opt/proxy/Caddyfile`, как показано ниже, затем выполните `docker compose up -d` в `/opt/proxy`.
- 04.Подключите каждый стек приложения к внешней сети `web` и запустите его через `docker compose up -d`.
- 05.Следите за выпуском первого сертификата командой `docker compose logs -f caddy` в `/opt/proxy`.
- 06.После правки Caddyfile перезагрузите конфиг без простоя: `docker compose exec -w /etc/caddy caddy caddy reload`.
# /opt/proxy/compose.yaml
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data # certificates and ACME account keys
- caddy_config:/config
networks:
- web
volumes:
caddy_data:
caddy_config:
networks:
web:
external: trueПодключите приложение и напишите Caddyfile
Каждый стек приложения объявляет ту же внешнюю сеть. Имя сервиса становится именем хоста, к которому подключается Caddy. Том `caddy_data` терять нельзя ни в коем случае: его удаление заставит Caddy заново запросить все сертификаты, что может упереться в лимиты ACME.
# /opt/app/compose.yaml
services:
app:
image: ghcr.io/example/app:latest
restart: unless-stopped
expose:
- "3000" # reachable on the network, NOT published on the host
networks:
- web
networks:
web:
external: true
# /opt/proxy/Caddyfile
{
email [email protected]
}
app.example.com {
reverse_proxy app:3000
}
grafana.example.com {
reverse_proxy grafana:3000
}
api.example.com {
reverse_proxy api-1:8080 api-2:8080 {
lb_policy round_robin
health_uri /healthz
health_interval 10s
}
}Бэкенды Caddy: апстримы, балансировка и health checks
В терминах Caddy бэкенд — это апстрим `reverse_proxy`. В одной директиве можно перечислить несколько апстримов, выбрать `lb_policy`, например `round_robin`, `least_conn` или `ip_hash`, и добавить активные health checks через `health_uri`, чтобы упавшие контейнеры выпадали из ротации. Пассивные проверки (`fail_duration`, `max_fails`) тоже полезны, если у приложения нет health-эндпоинта.
По умолчанию Caddy выставляет на проксируемых запросах `X-Forwarded-For`, `X-Forwarded-Proto` и `X-Forwarded-Host`. Настройте приложение доверять диапазону сети прокси, чтобы оно видело реальный IP клиента и строило ссылки `https://`. WebSocket и gRPC работают через `reverse_proxy` без дополнительных настроек; для gRPC без шифрования используйте `reverse_proxy h2c://service:50051`.
Метки вместо Caddyfile: caddy-docker-proxy
Те, кто ищет «Caddy proxy manager», обычно хотят того же, что даёт Nginx Proxy Manager: добавил контейнер — получил имя хоста и сертификат, не редактируя центральный файл. Официального GUI у Caddy нет, но проект сообщества caddy-docker-proxy (github.com/lucaslorentz/caddy-docker-proxy) генерирует Caddyfile из меток контейнеров и перезагружает Caddy при запуске и остановке контейнеров.
Ему нужен доступ на чтение к Docker-сокету, а это фактически root на хосте. Монтируйте сокет только для чтения, обновляйте образ прокси и предпочитайте обычный Caddyfile, если у вас всего несколько сервисов.
services:
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- CADDY_INGRESS_NETWORKS=web
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- caddy_data:/data
networks:
- web
whoami:
image: traefik/whoami
networks:
- web
labels:
caddy: whoami.example.com
caddy.reverse_proxy: "{{upstreams 80}}"
volumes:
caddy_data:
networks:
web:
external: trueОбычный Caddyfile против caddy-docker-proxy
| Обычный Caddyfile | Метки caddy-docker-proxy | |
|---|---|---|
| Где живёт конфигурация | Один файл, который вы версионируете в git | Метки, разбросанные по compose-файлам |
| Доступ к Docker-сокету | Не нужен | Обязателен (чувствительно для безопасности) |
| Новый сервис | Правка Caddyfile, `caddy reload` | Добавить метки, запустить контейнер |
| Образ | Официальный `caddy:2` | Образ сообщества на базе Caddy |
| Отладка | Читаете файл напрямую | Смотрите сгенерированный конфиг в логах |
| Лучше всего для | Небольших стабильных VPS | Множества короткоживущих или часто меняющихся сервисов |
Запуск на VPS
На KVM VPS NoctisNode у вас полный root и выделенный IPv4, поэтому опубликовать 80 и 443 из одного контейнера Caddy и спрятать всё остальное несложно. Wisp (1 vCPU, 1 GB RAM) хватит для Caddy и пары лёгких контейнеров; более тяжёлые стеки поместятся на Pulse или Umbra. Детали выпуска сертификатов и типичные сбои описаны в статье как работает автоматический HTTPS в Caddy, а пересылка сырого TCP и SSH — в проксировании TCP и SSH в Caddy. Закройте SSH хоста по руководству по защите SSH.
FAQ
QКак настроить Caddy как reverse proxy для Docker-контейнера?
AПоместите Caddy и контейнер в одну пользовательскую Docker-сеть и используйте в Caddyfile `reverse_proxy service-name:port`. Не используйте `localhost`: внутри контейнера Caddy он указывает на сам Caddy.
QПочему Caddy отдаёт 502 Bad Gateway для моего контейнера?
AОбычно контейнеры не в общей сети, порт указан неверно (нужен внутренний порт контейнера, а не опубликованный порт хоста) или приложение внутри контейнера слушает только 127.0.0.1. Пусть приложение слушает 0.0.0.0, и проверьте `docker network inspect web`.
QБезопасно ли использовать caddy-docker-proxy?
AОн работает хорошо, но требует Docker-сокет, а это контроль над хостом на уровне root. Монтируйте сокет только для чтения, обновляйте образ и не открывайте наружу admin-эндпоинт Caddy.
QГде Caddy хранит сертификаты в Docker?
AВ `/data` внутри официального образа, поэтому смонтируйте туда именованный том. Потеря тома означает перевыпуск всех сертификатов и возможное упирание в лимиты ACME.
QЕсть ли Caddy proxy manager с веб-интерфейсом?
AУ самого Caddy нет официального веб-интерфейса; он настраивается через Caddyfile, JSON или admin API. Инструменты на метках вроде caddy-docker-proxy дают похожий сценарий «добавил контейнер — и готово».