Перейти к основному содержимому

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-провайдера или регистратора:

ТипИмяЗначение
ApanelIPv4-адрес сервера
AsubIPv4-адрес сервера
AAAApanelIPv6-адрес сервера
AAAAsubIPv6-адрес сервера

В 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
/opt/caddy/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​

/opt/caddy/docker-compose.yml
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 не включён.

Что дальше​