Caddy
Обзор
Caddy — обратный прокси, который сам получает и продлевает сертификаты Let's
Encrypt: отдельный клиент ACME, cron и ручная перезагрузка не нужны. Панель XLADA
и страница подписки работают только за прокси с HTTPS — их middleware разрывает
соединение, если запрос пришёл без заголовков X-Forwarded-For и
X-Forwarded-Proto: https.
Caddy проставляет X-Forwarded-* самостоятельно. В конфигурации ниже мы всё
равно задаём их явно: так поведение не изменится, если перед Caddy появится CDN
или балансировщик, а X-Forwarded-Proto не превратится в http.
В примерах домены panel.example.com и sub.example.com, апстримы
127.0.0.1:3000 (панель) и 127.0.0.1:3010 (страница подписки) — именно эти
порты публикуют на loopback хоста контейнеры xpanel и xpanel-subpage.
Продолжить можно только с зарегистрированным доменом: без него Let's Encrypt не выдаст сертификат.
DNS
Проверьте IP-адрес сервера (лучше статический) и создайте записи у DNS-провайдера или регистратора:
| Тип | Имя | Значение |
|---|---|---|
A | panel | IPv4-адрес сервера |
A | sub | IPv4-адрес сервера |
AAAA | panel | IPv6-адрес сервера |
AAAA | sub | IPv6-адрес сервера |
В Cloudflare записи добавляются в разделе DNS → Records → Create record: тип
A, в поле имени — panel или sub, в поле адреса — IP сервера.
- Серое облако — трафик идёт напрямую на ваш сервер, Caddy видит реальный IP клиента. Выбирайте этот режим, если Cloudflare недоступен или мешает.
- Оранжевое облако — трафик проходит через сеть Cloudflare: IP сервера скрыт,
работает защита от атак. Здесь обязательно укажите в Caddyfile диапазоны
Cloudflare в
trusted_proxies static, иначе все клиенты будут выглядеть как один адрес Cloudflare, а HWID-лимиты, аудит и защита входа от перебора потеряют смысл.
Дождитесь обновления записей: dig +short panel.example.com должен вернуть
адрес сервера, а при оранжевом облаке — адреса Cloudflare.
Сертификаты
Caddy получает сертификат автоматически, как только домен указан в Caddyfile.
Для проверки домена нужны открытые порты 80 (HTTP-01) и 443 (TLS-ALPN-01);
продление также автоматическое — примерно за 30 дней до истечения срока. В
глобальном блоке достаточно указать email: на этот адрес придут уведомления о
проблемах с сертификатом.
Полный Caddyfile
Создайте каталог и файл:
sudo mkdir -p /opt/caddy && cd /opt/caddy
sudo nano Caddyfile
# ─── Глобальные параметры ────────────────────────────────────────────
{
# Почта для уведомлений Let's Encrypt
email admin@example.com
# Реальный IP клиента за Cloudflare. Диапазоны публикует Cloudflare;
# их список стоит периодически сверять с актуальным.
servers {
trusted_proxies static \
173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 \
141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 \
197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 \
104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 \
2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 \
2a06:98c0::/29 2c0f:f248::/32
client_ip_headers CF-Connecting-IP X-Forwarded-For
}
}
# ─── Панель XLADA ────────────────────────────────────────────────────
panel.example.com {
encode zstd gzip
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
# Статика сборки: хеш в имени файла позволяет кэшировать её надолго
@assets path /assets/*
header @assets Cache-Control "public, max-age=31536000, immutable"
reverse_proxy 127.0.0.1:3000 {
# Панель рвёт соединение без этих двух заголовков.
# X-Forwarded-For перезаписываем, чтобы клиент не подделал свой IP.
header_up X-Forwarded-For {client_ip}
header_up X-Forwarded-Proto https
header_up X-Real-IP {client_ip}
header_up Host {host}
# WebSocket: Caddy сам обрабатывает Upgrade и Connection,
# отдельные заголовки не нужны. flush_interval -1 отключает
# буферизацию — это важно для SSE и стриминга статистики.
flush_interval -1
}
}
# ─── Страница подписки ───────────────────────────────────────────────
sub.example.com {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
# Статика сборки: хеш в имени файла позволяет кэшировать её надолго
@assets path /assets/*
header @assets Cache-Control "public, max-age=31536000, immutable"
reverse_proxy 127.0.0.1:3010 {
header_up X-Forwarded-For {client_ip}
header_up X-Forwarded-Proto https
header_up X-Real-IP {client_ip}
header_up Host {host}
flush_interval -1
}
}
# ─── Запросы по IP и на неизвестные домены ───────────────────────────
:443 {
tls internal
respond 204
}
Ключевые директивы:
| Директива | Назначение |
|---|---|
trusted_proxies static | доверять заголовкам Cloudflare из этих подсетей |
client_ip_headers | брать реальный IP из CF-Connecting-IP |
header_up X-Forwarded-For | передать настоящий IP клиента в панель |
header_up X-Forwarded-Proto https | панель принимает только https |
flush_interval -1 | потоковая отдача без буферизации |
@assets и header | длинный кэш для статики сборки из /assets/ |
:443 с tls internal | отсекает сканеры, стучащиеся по IP |
Не добавляйте на домен sub.example.com дополнительную HTTP-аутентификацию:
страницу подписки запрашивают клиентские приложения, которые не умеют вводить
логин и пароль. Вход в панель защищайте её собственной аутентификацией и TOTP.
Запуск в Docker
services:
caddy:
image: caddy:2.9
container_name: caddy
restart: unless-stopped
# host-режим нужен, чтобы Caddy видел 127.0.0.1:3000 и 127.0.0.1:3010
network_mode: host
environment:
- TZ=UTC
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./data:/data # сертификаты и ключи ACME
- ./config:/config # служебное состояние Caddy
cd /opt/caddy
docker compose up -d
docker compose logs -f -t
Host-режим выбран намеренно: только так контейнер Caddy видит loopback-порты
панели и страницы подписки, а сертификаты остаются на хосте. Порты 80 и 443
слушает сам Caddy, а 3000, 3010 и 3001 наружу не выходят.
Проверка и перезагрузка
# Проверка синтаксиса без перезапуска
caddy validate --config /opt/caddy/Caddyfile
# Приведение файла к каноническому виду
caddy fmt --overwrite /opt/caddy/Caddyfile
# Мягкая перезагрузка: соединения не разрываются
systemctl reload caddy
# В Docker те же команды выполняются внутри контейнера
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
Что смотреть в логах
journalctl -u caddy -f # установка на хост
docker compose logs -f caddy # запуск в Docker
certificate obtained successfully— сертификат выдан;serving initial configuration— конфигурация принята;tls.obtainилиacme— проверьте DNS, порты80и443, лимиты Let's Encrypt;dial tcp 127.0.0.1:3000: connect: connection refused— панель не запущена.
Убедитесь, что оба сайта отвечают:
curl -I https://panel.example.com
curl -I https://sub.example.com
Ответ должен начинаться с HTTP/2 200. Реальный IP клиента удобно проверить в
аудите панели: если там адреса Cloudflare, значит trusted_proxies не включён.