Цель: поднять устойчивый VLESS-сервер (Xray через 3x-ui), спрятанный за TLS и CDN Cloudflare, чтобы клиенты подключались по обычному https://-домену, а не к «голому» IP и нестандартным портам.
В статье разбираем полный рабочий вариант:
3x-ui/Xray в Docker
Caddy как reverse proxy и TLS-терминатор
Cloudflare как фронт и маскировка трафика
VLESS + WebSocket + TLS
Автоматическая генерация корректных ссылок / QR-кодов для v2rayNG через 3x-ui (без ручного редактирования профилей)
В примерах используем домен:
основной домен:
your.siteVPN-сабдомен:
vpn.your.site
Подставьте свои домены и пути, где нужно.
1. Архитектура решения
Цепочка выглядит так:
Клиент (v2rayNG, etc.)
⇅ TLS+WS (443, wss://vpn.your.site/ws)
Cloudflare (оранжевое облако, HTTPS proxy)
⇅ HTTPS
Caddy (порт 443 на сервере)
⇅ ws (локально)
Xray (3x-ui, VLESS+WS без TLS на 10000 порту)
⇅ Интернет
Ключевые моменты:
Клиент всегда ходит на
vpn.your.site:443по TLS.Xray внутри сервера слушает локальный порт 10000 без TLS.
Caddy принимает HTTPS, делает TLS-рукопожатие и проксирует WebSocket на Xray.
Cloudflare видит обычный
https://-запрос и не отличает его от легитимного сайта.
2. Требования к серверу
Linux-сервер/VPS с публичным IP
Установленные Docker и Docker Compose
Зарегистрированный домен, подключённый к Cloudflare
Пусть файлы стека лежат в /opt/vpn-stack.
sudo mkdir -p /opt/vpn-stack/{db,cert,caddy,www,caddy/data,caddy/config}
cd /opt/vpn-stack
Создадим простую заглушку-сайт, который будет открываться в браузере (полезно для проверки HTTPS):
cat > www/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>your VPN</title>
</head>
<body>
<h1>your.site</h1>
<p>Service node is up and running.</p>
</body>
</html>
EOF
3. Docker Compose: 3x-ui + Caddy
Файл docker-compose.yml:
services:
3x-ui:
image: ghcr.io/mhsanaei/3x-ui:latest
container_name: 3x-ui
hostname: vpn.your.site
volumes:
- ./db:/etc/x-ui
- ./cert:/root/cert
environment:
XRAY_VMESS_AEAD_FORCED: "false"
XUI_ENABLE_FAIL2BAN: "true"
TZ: "Europe/Amsterdam"
restart: unless-stopped
networks:
- proxy
caddy:
image: caddy:2.8-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
- ./caddy/config:/config
- ./www:/var/www/html
networks:
- proxy
networks:
proxy:
driver: bridge
Здесь:
3x-ui работает только внутри Docker-сети
proxy.Caddy - единственный сервис, который слушает порты 80/443 наружу.
vpn.your.site- замените на ваш домен
4. Конфигурация Caddy
Файл caddy/Caddyfile:
{
email admin@your.site
}
vpn.your.site {
encode gzip
# === VLESS WebSocket → Xray (порт 10000) ===
reverse_proxy /ws* 3x-ui:10000
# === Всё остальное → панель 3x-ui (порт 2053) ===
reverse_proxy 3x-ui:2053
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
-Server
-X-Powered-By
}
}
Что здесь важно:
Caddy сам получит сертификат Let’s Encrypt для
vpn.your.site.Любой запрос на
https://vpn.your.site/wsпроксируется на3x-ui:10000(VLESS+WS).Всё остальное (
/,/panel,/login,/assets/...) уходит на3x-ui:2053— панель и её статика.
5. Первый запуск стека
Из каталога /opt/vpn-stack:
docker compose up -d
Проверяем, что оба контейнера живы:
docker ps
Должны быть 3x-ui и caddy в статусе Up.
Первые минуты Caddy может быть занят получением сертификата. Логи:
docker logs --tail=50 caddy
После успешного получения сертификата в логах будет certificate obtained successfully.
6. Настройка Cloudflare
DNS
В панели Cloudflare для зоны yuor.site создаём запись:
Type:
AName:
vpnIPv4 address: IP вашего сервера
Proxy status:
для первичного теста можно поставить DNS only (серое облако),
после тестов — Proxied (оранжевое облако).
SSL/TLS режим
В разделе SSL/TLS → Overview:
Режим: Full или Full (strict) (рекомендуется Full strict).
Убедитесь, что Universal SSL для зоны активен, и для *.your.site/your.site есть действующий edge-сертификат.
7. Первый заход в панель 3x-ui
Пока Cloudflare в режиме DNS only (серое облако):
Откройте в браузере:
https://vpn.your.site/Должна открыться страница панели 3x-ui (логин/редирект на
/panelили/login).Войдите под стандартным логином/паролем (если не меняли), сразу измените их.
Панель отвечает — значит Caddy + 3x-ui связаны корректно.
После этого можно включить оранжевое облако (Proxied) и убедиться, что https://vpn.your.site/ продолжает открываться без ошибок SSL.
8. Создание рабочего VLESS+WS inbound в 3x-ui
Теперь настроим входящий VLESS-инбаунд, который будет работать за Caddy.
8.1. Базовые поля инбаунда
В панели 3x-ui:
Перейдите в Inbounds / Инбаунды.
Нажмите Add / Добавить.
Заполните верхнюю часть:
Протокол:
VLESSВключить: да
Адрес: пусто или
0.0.0.0Порт:
10000Примечание: например,
vless-ws-10000
Остальные лимиты (трафик, срок действия) — по вашему усмотрению.
8.2. Клиент (user)
В блоке клиентов:
Добавьте пользователя (кнопка
+):ID (UUID): сгенерируйте кнопкой Generate
Email/Комментарий: для себя, например
user@your.site
Этот UUID будем использовать в клиентах (v2rayNG и т.п.).
8.3. Транспорт WebSocket (ws)
Ниже, в блоке транспорта:
Network / Транспорт:
ws(WebSocket)Path / Путь:
/wsHost:
vpn.your.siteБезопасность:
none(пусто, TLS на уровне Xray не включаем)
Остальные поля (SNI, ALPN, uTLS, сертификаты) оставляем по умолчанию.
8.4. Sniffing
Для начала можно оставить выключенным. При желании включите позже для авто-распознавания доменов.
Сохраните инбаунд.
9. Настройка External Proxy (чтобы QR/ссылки были сразу правильные)
По умолчанию 3x-ui будет генерировать vless-URL вида:
vless://UUID@vpn.your.site:10000?...&security=none
Это удобно только при прямом подключении к порту 10000. В нашем случае клиенты должны ходить на vpn.your.site:443 по TLS.
Чтобы 3x-ui сам подставлял 443 и TLS, включаем External Proxy.
Откройте только что созданный инбаунд на редактирование.
Пролистайте вниз до блока External Proxy.
Включите переключатель External Proxy.
Заполните поля:
Security / Тип:
tlsHost / Address:
vpn.your.sitePort:
443(Опционально) uTLS:
chrome(Опционально) ALPN:
h2,http/1.1
Сохраните инбаунд.
Теперь в подробностях клиента URL будет примерно таким:
vless://UUID@vpn.your.site:443?type=ws&encryption=none&path=%2Fws&host=vpn.your.site&security=tls#Имя
Этот URL / QR-код уже корректен для v2rayNG, Shadowrocket и других клиентов — никаких ручных правок порта/ TLS не нужно.
10. Настройка клиента (пример для v2rayNG)
Теперь можно либо отсканировать QR-код из 3x-ui, либо добавить профиль вручную.
10.1. Ожидаемые параметры профиля
После импорта QR/URI профиль в v2rayNG должен выглядеть так:
Type / Protocol:
VLESSServer / Address:
vpn.your.sitePort:
443ID (UUID): ваш UUID из 3x-ui
Encryption:
noneTLS: включен (ON)
SNI / Server name:
vpn.your.siteTransport / Network:
wsWS path:
/wsWS Host:
vpn.your.site
Если всё совпадает — профиль готов.
10.2. Тест
Подключитесь в v2rayNG → откройте любой сайт.
В панели 3x-ui в этом инбаунде и у конкретного пользователя появится:
статус «Был(а) в сети» со временем последнего подключения
байты в колонке «Использование» начнут расти
11. Проверка и типичные проблемы
11.1. TLS / SSL ошибки в браузере
Если https://vpn.your.site/ выдаёт ERR_SSL_VERSION_OR_CIPHER_MISMATCH или 525/526:
проверьте, что Caddy успешно получил сертификат (логи caddy)
проверьте, что в Cloudflare:
SSL mode = Full / Full strict
Universal SSL активен, сертификат для домена выпущен
11.2. net/http: TLS handshake timeout в клиенте
Чаще всего связано с тем, что:
Xray не слушает тот порт, на который проксирует Caddy (проверьте, что инбаунд на 10000, а не на 443);
или XRAY падает из-за лишнего инбаунда с TLS и пустыми сертификатами (ищите ошибки в логах 3x-ui).
11.3. io: read/write on closed pipe / connect: connection refused
Появляется, когда Caddy не может подключиться к 3x-ui:10000:
XRAY не запущен или падает на старте;
инбаунд выключен или порт изменён;
ошибка в Caddyfile (не тот порт, опечатка в
3x-ui:10000).
12. Добавление второго инбаунда (опционально)
Если нужно несколько независимых профилей (например, разные лимиты/страны), можно добавить второй инбаунд.
12.1. В 3x-ui
Создайте ещё один инбаунд:
Протокол:
VLESSАдрес:
0.0.0.0Порт:
20000Транспорт:
wsPath:
/ws2Host:
vpn.your.siteБезопасность:
none
Включите External Proxy:
Security:
tlsHost:
vpn.your.sitePort:
443
12.2. В Caddyfile
Добавьте строку:
reverse_proxy /ws2* 3x-ui:20000
Теперь:
/wsидёт на порт 10000/ws2идёт на порт 20000
Клиенты будут отличаться только path (/ws vs /ws2).
13. Безопасность панели и эксплуатация
Несколько рекомендаций для продакшен-использования:
Смените стандартный логин/пароль 3x-ui на сложные.
Закройте панель по IP (через firewall), Cloudflare Access или вынесите её на отдельный поддомен (например,
panel.your.site).Регулярно обновляйте образ
ghcr.io/mhsanaei/3x-uiи Caddy.Делайте резервные копии каталога
./db(там конфигурация 3x-ui и пользователи).Следите за нагрузкой CPU и сетевым трафиком (
htop,iftop) при росте числа клиентов.
14. Заключение
Мы собрали полный рабочий стек:
Xray/3x-ui в Docker как ядро VLESS-сервера
Caddy как удобный и безопасный TLS-терминатор и reverse proxy
Cloudflare как фронтовый слой и маскировка под обычный HTTPS-сайт
VLESS + WebSocket + TLS, настроенный так, что клиенты получают корректные профили по ссылке или QR-коду без ручных доработок
Такой подход хорошо масштабируется:
можно добавлять новые инбаунды (другие пути и порты)
выносить панель на отдельный домен
переключаться на более современные транспорты (XHTTP/H2/H3), когда они окончательно заменят WebSocket
Главное — соблюдать разделение ролей:
Xray слушает локальные порты без TLS;
Caddy занимается TLS и маршрутизацией;
Cloudflare скрывает реальный origin и отдаёт только «чистый» HTTPS.
На основе этой базовой схемы можно строить как личный VPN, так и более сложные конфигурации для нескольких пользователей и групп, сохраняя при этом управляемость и прозрачную структуру конфигов.