Классический сценарий для PasarGuard — форка Marzban — это не «одна коробка со всем сразу», а связка из двух машин: отдельная панель и отдельная нода, отдающая трафик. Именно так и стоит запускать прод: панель хранит пользователей и раздаёт подписки, а нода крутит Xray-ядро и принимает клиентов. Ниже — полный разбор такой раздельной установки, где вместо игрушечного Shadowsocks сразу поднимается VLESS + Reality с маскировкой под чужой сайт, а панель закрывается боевым TLS от Let's Encrypt.
Отдельный акцент — на восьми местах, где инсталляция ломается молча: интерфейс при этом не говорит ничего внятного, и почти любую поломку приходится ловить в docker logs. Всё, что здесь описано, снято с реального стенда: панель v5.2.1, нода v0.5.3, ядро Xray 26.3.27. Настраивать будем преимущественно через веб-панель — со скриншотами конкретных экранов; в терминал уходим только за тем, что иначе не сделать.
Договоримся об обозначениях. Домен в примерах — pg-test.gig.ovh. На скриншотах IP заменены документационными (203.0.113.10 — нода, 198.51.100.10 — панель), приватный ключ Reality, api_key ноды и UUID клиента — плейсхолдеры. Везде подставляйте своё.
Как всё устроено
Панель и нода живут на разных серверах намеренно. Панель — это «мозг»: пользователи, ядра, хосты, подписки, статистика. Нода — «мышцы»: на ней работает Xray и держит клиентские соединения. Связывает их управляющий gRPC-канал под самоподписанным TLS.
Сервер A (панель): PasarGuard panel + TimescaleDB + Caddy. Наружу открыты только 80 и 443.
Сервер B (нода): PasarGuard node + Xray. Слушает управляющий 62050 от панели и клиентов VLESS Reality на 443.
Сколько ресурсов нужно
Обе машины стенда — почти одинаковые VPS. Цифры ниже сняты вживую; это рабочий минимум под одну ноду и несколько пользователей, без свопа.
Параметр
Сервер A (панель)
Сервер B (нода)
ОС
Ubuntu 26.04 LTS
Ubuntu 26.04 LTS
Ядро
7.0.0-14-generic
7.0.0-14-generic
Архитектура
x86_64 (KVM)
x86_64 (KVM)
vCPU
2
2
ОЗУ
3.3 ГБ
3.3 ГБ
Диск
99 ГБ (занято ~5 ГБ)
79 ГБ (занято ~4 ГБ)
Больше всех на панели ест не сама панель, а TimescaleDB: образ ~2 ГБ и несколько сотен мегабайт в покое. С SQLite панели хватило бы и 1 ГБ, но тогда пропадает статистика по нодам (об этом ниже). Под Timescale держите в уме 2 ГБ.
Порты
Порт
Где
Наружу?
Зачем
22
A, B
да (лучше сузить)
SSH
80
A
да
HTTP → редирект, ACME
443
A
да
HTTPS: панель и подписки
8000
A
нет (localhost)
uvicorn за Caddy
5432
A
нет (127.0.0.1)
TimescaleDB
62050
B
только с IP панели
gRPC панель↔нода
443
B
да
инбаунд VLESS Reality
443 фигурирует дважды, но на разных хостах: на A его держит Caddy, на B — Xray. Пересечения нет.
База: почему Timescale, а не SQLite
SQLite в PasarGuard есть, и для одной локальной ноды его достаточно. Но выносите ноды на отдельные серверы и захотите графики по каждой — упрётесь в ограничение прямо из .env.example:
## Usage recording. Node stats recording is only available for PostgreSQL/TimescaleDB.
# ENABLE_RECORDING_NODES_STATS = False
Запись статистики по нодам живёт только на PostgreSQL/TimescaleDB; на SQLite флаг мёртвый, и раздел статистики по узлам просто исчезает. Поэтому база — TimescaleDB (это PostgreSQL с расширением под временные ряды, что удобно для поминутного трафика).
Важно. Базу выбирают до первого старта — переезд SQLite→PostgreSQL на живой панели болезненный. Есть шанс на вторую ноду и графики — сразу Timescale.
Что приготовить заранее
Чтобы не спотыкаться посреди установки, соберите это до первой команды: два VPS с root и их публичные IP (PANEL_IP, NODE_IP); домен для панели, делегированный на NS Cloudflare (DNS-01 ходит через её API); аккаунт Cloudflare и токен с правами Zone:DNS на зону (My Profile → API Tokens → Create Token, шаблон Edit zone DNS, в Zone Resources — ваша зона; значение показывается один раз); и проверьте, что нужные порты не закрыты у провайдера. A-запись на домен заведите сразу — пока ставится Docker, она успеет разойтись по DNS.
Готовим обе машины
Docker ставим официальным скриптом — он сам подцепит репозиторий и compose-плагин. Одинаково для A и B:
# от root
curl -fsSL https://get.docker.com | sh
docker --version # Docker version 29.7.2
docker compose version # Docker Compose version v5.5.0
Проверьте, что доступен именно docker compose (v2-плагин через пробел) — на нём построен весь гайд.
Сервер A: база и панель
Заводим каталог и один раз генерируем секреты — пароль БД и api_key (общий секрет панели и ноды):
Посмотреть значения в любой момент — cat /opt/pasarguard/.secrets. Ниже по тексту они идут плейсхолдерами и подставляются руками: DB_PASS — в .env и в compose базы, API_KEY — позже на сервере B и в карточке ноды (забрать: grep API_KEY /opt/pasarguard/.secrets).
Значение
Где берётся
Куда подставляется
PANEL_IP
публичный IP сервера A
A-запись домена, правило ufw на ноде
NODE_IP
публичный IP сервера B
карточка ноды, адрес хоста, SAN сертификата
DB_PASS
openssl rand -hex 24 на A
.env панели и compose базы
API_KEY
UUID на A (.secrets)
compose ноды и поле API Key в карточке
домен
ваш, на NS Cloudflare
Caddyfile, VITE_BASE_API, ALLOWED_ORIGINS
CF_API_TOKEN
Cloudflare → API Tokens
compose Caddy
PRIVATE_KEY / PUBLIC_KEY
docker exec pg-node xray x25519
приватный — в инбаунд; публичный панель подставит в ссылку
shortId
openssl rand -hex 8
инбаунд Core Config (shortIds)
Грабля №1: формат .env и падение --env-file
Оригинальный .env.example идёт «с пробелами вокруг равно»: UVICORN_HOST = "0.0.0.0". Python-овский dotenv это переваривает, а докеровский --env-file — нет:
Пробел утёк в имя переменной ('UVICORN_HOST '). Лечится тем, что .env пишется без пробелов, строго KEY=value. Полный файл с комментариями:
# ==== PasarGuard panel .env (чистый KEY=value, без пробелов) ====
# Слушать только localhost: наружу панель выставляет Caddy.
UVICORN_HOST=127.0.0.1
UVICORN_PORT=8000
# Подключение к базе. Драйвер asyncpg идёт в образе панели.
SQLALCHEMY_DATABASE_URL=postgresql+asyncpg://pasarguard:DB_PASS@127.0.0.1:5432/pasarguard
# Статистика по нодам — только на PostgreSQL/TimescaleDB.
ENABLE_RECORDING_NODES_STATS=True
DISABLE_RECORDING_NODE_USAGE=False
SUBSCRIPTION_PATH=sub
DASHBOARD_PATH=/dashboard/
LOG_LEVEL=INFO
# ==== работа за reverse-proxy Caddy (домен поднимем следующим шагом) ====
UVICORN_PROXY_HEADERS=True
UVICORN_FORWARDED_ALLOW_IPS=127.0.0.1
VITE_BASE_API=https://pg-test.gig.ovh/
ALLOWED_ORIGINS=https://pg-test.gig.ovh
Грабля №2: образы из Docker Hub и 429
В штатном compose образ — pasarguard/panel:latest, то есть Hub, который на пуллах любит отдавать 429 Too Many Requests. Берём зеркало ghcr: ghcr.io/pasarguard/panel:latest и ghcr.io/pasarguard/node:latest. Compose сервера A:
Поднимаем по очереди — сначала БД до healthy, затем панель:
cd /opt/pasarguard
docker compose up -d # очерёдность обеспечит depends_on: compose дождётся healthy у базы
docker logs -f pasarguard
Грабля №3: без TLS панель слушает только localhost
После миграций в логе появится предупреждение — не пугайтесь, оно ожидаемо:
IMPORTANT!
You're running PasarGuard without specifying UVICORN_SSL_CERTFILE and UVICORN_SSL_KEYFILE.
The application will only be accessible through localhost...
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Это и есть задумка: панель на 127.0.0.1:8000, а TLS и внешний доступ обеспечит Caddy. Поэтому в .env и стоит UVICORN_HOST=127.0.0.1. Ниже будет и Attention: You have no node — ноду добавим позже.
Открываем панель наружу: Caddy и TLS через Cloudflare DNS-01 (грабля №4)
Панель доступна только на localhost — ставим перед ней Caddy с сертификатом Let's Encrypt по DNS-01 (открытый 80 для ACME не нужен). И тут грабля: обычный caddy:2-alpine не умеет Cloudflare — нужен билд с DNS-модулем, ghcr.io/caddybuilds/caddy-cloudflare:latest. В нём модуль dns.providers.cloudflare уже вкомпилирован, и этот же образ идёт в compose ниже — отдельной команды под проверку не нужно: будь модуль не на месте, Caddy упал бы на directive dns cloudflare с понятной ошибкой.
В зоне Cloudflare заводим A-запись pg-test.gig.ovh → PANEL_IP в режиме DNS only (серое облако). Caddyfile — это reverse-proxy на панель плюс TLS через DNS-01; Caddy сам возьмёт настоящий, доверенный браузерами сертификат Let's Encrypt:
mkdir -p /opt/caddy && cd /opt/caddy # сюда кладём Caddyfile и docker-compose.yml
docker compose up -d
docker logs -f caddy
# http.acme_client trying to solve challenge challenge_type=dns-01
# http.acme_client authorization finalized authz_status=valid
# tls.obtain certificate obtained successfully
Во время челленджа в зоне на секунды появляется TXT _acme-challenge.pg-test.gig.ovh и затем убирается — так работает DNS-01. Проверяем сертификат снаружи:
Готово: панель открывается по https://pg-test.gig.ovh с валидным сертификатом, дальше работаем в браузере. Опционально: при отладке, чтобы не жечь лимиты боевого Let's Encrypt, можно временно добавить в глобальный блок Caddyfile строку acme_ca https://acme-staging-v02.api.letsencrypt.org/directory (сертификат будет недоверенный — это норма для проверки), затем убрать её, удалить /var/lib/caddy/data/caddy/certificates и перезапустить Caddy.
Грабля №5: владелец и мёртвый SUDO_USERNAME
Попытка войти статическим админом из .env в 5.2.1 упирается в отлуп:
env admin not allowed in production
В проде env-админ запрещён специально. Владелец создаётся одноразовым ключом через CLI, и тут вторая ловушка: команды pasarguard в PATH нет, cli_wrapper.sh не исполняемый. Работает только прямой вызов Python внутри контейнера (рабочий каталог /code):
docker exec pasarguard python /code/pasarguard-cli.py generate-temp-key
# Temp key: 3e2a706a-....
# This key is valid for 5 minutes and can only be used once.
Ключ действует 5 минут и однократно. Панель уже открывается по домену — идём на https://pg-test.gig.ovh/dashboard/, на логине жмём Owner access, режим Create и вводим ключ, логин и пароль владельца.
После входа попадаем на дашборд — дальше почти всё мышкой.
Карта интерфейса: за что отвечает каждая вкладка
Меню слева — это весь PasarGuard целиком. Коротко по каждому пункту.
Dashboard — сводка: онлайн, активные, суммарный трафик, состояние нод. Только мониторинг.
Users — сердце панели: создание клиентов, их лимиты, сроки, статусы, привязка к группам, а по иконкам в строке — ссылка-подписка и QR. Тут проходит основная ежедневная работа.
Statistics — графики трафика по пользователям и по нодам; работают только на PostgreSQL/TimescaleDB.
Hosts — «витрина» подключений: инбаунд превращается в клиентскую ссылку только через хост (адрес, порт, SNI, отпечаток). Один инбаунд может иметь несколько хостов.
Groups — набор инбаундов, который выдаётся пользователю; удобно делать «тарифы».
Admins и Admin Roles — дополнительные администраторы и ограничение их прав (например, реселлеры).
API Keys — ключи для автоматизации и биллинга.
Nodes — узлы и ядро. Вкладки: Nodes (список и статусы), Core Configs (конфиги Xray: инбаунды, исходящие, маршрутизация), WireGuard (подсети/пиры WG) и Logs (живые логи ядра с ноды).
Templates — шаблоны страниц подписки и настроек пользователей по умолчанию.
Дальше мы и пройдём по этой карте: подключим ноду (Nodes), настроим на ней протокол (Core Configs), выставим наружу (Hosts), соберём в тариф (Groups) и выдадим клиенту (Users).
Сервер B: нода и её сертификат
Грабля №6: без сертификата нода не стартует, а без SAN — не подключается
Канал панель↔нода шифруется TLS, и сертификат обязан существовать до первого запуска, иначе контейнер циклится:
open /var/lib/pg-node/certs/ssl_cert.pem: no such file or directory
Сделали сертификат «обычной» командой без SAN — панель отвергает ноду на подключении:
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
IP address mismatch, certificate is not valid for 'NODE_IP'
Панель сверяет именно SAN с IP, а не CN. Правильная генерация добавляет subjectAltName=IP:...:
mkdir -p /opt/pg-node && cd /opt/pg-node # сюда кладём docker-compose.yml
docker compose up -d
docker logs pg-node
# Starting Node: v0.5.3
# gRPC Server listening on 0.0.0.0:62050
Строку Failed to load env file ... open .env игнорируем — переменные пришли через environment:.
Файрвол ноды
Управляющий 62050 открываем только для IP панели, SSH разрешаем до включения ufw:
ufw allow 22/tcp
ufw allow from PANEL_IP to any port 62050 proto tcp # управляющий канал — только с панели
ufw allow 443/tcp # клиентский инбаунд VLESS Reality
ufw --force enable
ufw status numbered
Политика ufw по умолчанию — deny incoming, поэтому отдельное запрещающее правило для 62050 не нужно: всё, что не разрешено явно, отбрасывается ещё на SYN.
Безопасность. С посторонней машины nc -zv NODE_IP 62050 обязан таймаутиться, с панели — отвечать succeeded. Управляющий канал даёт полный доступ к ядру ноды, в открытый интернет его выпускать нельзя.
Подключаем ноду в панели (Nodes)
В Nodes → Nodes жмём Create Node. Форма короткая, но одно поле коварно — и при ошибке причина видна только в docker logs pasarguard, тогда как в UI просто краснеет точка.
Поле
Значение
Комментарий
Node Name
test-node-1
любое имя
Node Address
NODE_IP
публичный IP сервера B
Node Port
62050
gRPC-порт
Core Configuration
Default Core Config
какое ядро применить
API Key
API_KEY
UUID из переменных ноды
Certificate
ssl_cert.pem с ноды
целиком, с BEGIN/END
Грабля №7: не тот сертификат в карточке
В поле Certificate (в API — server_ca) кладут сертификат с ноды целиком. Подсунете чужой (скажем, свежесгенерированный на панели) — отвал, и причина снова только в логе:
Сертификат берём так: cat /var/lib/pg-node/certs/ssl_cert.pem. Успех в логе панели:
Node-operation - New node "test-node-1" with id "1" added by admin "admin"
Node-operation - Connecting to "test-node-1" node
Node-operation - Connected to "test-node-1" node v0.5.3, core run on v26.3.27
А в списке нод точка зеленеет, появляются версии ноды (0.5.3) и ядра (26.3.27):
Красный статус? Не гадайте по UI — открывайте docker logs pasarguard.
VLESS + Reality: узел, который выглядит как чужой сайт
Дефолтный Shadowsocks хорош лишь для проверки «а оно вообще заводится». Боевой протокол на Xray сегодня — VLESS + Reality. Суть: нода не поднимает собственный TLS (который сам по себе выдаёт прокси), а на рукопожатии подделывается под чужой сайт, заимствуя TLS у реального внешнего хоста (например, www.microsoft.com). Снаружи 443 на ноде неотличим от настоящего Microsoft: тот же сертификат, та же цепочка. Своего домена и сертификата для клиента не требуется.
Шаг 1. Ключи Reality
Reality опирается на пару x25519 и короткий shortId. Ключи выдаёт само ядро прямо в контейнере ноды:
Оставляем галку Restart Nodes и сохраняем — панель раскатает конфиг на ноду и перезапустит ядро. В списке появляется VLESS Reality / vless / 443:
Про «жертву».dest/serverNames должны указывать на реально живой чужой сайт на 443 с TLS 1.3, не связанный с вами и не заблокированный у ваших клиентов (крупные CDN, www.microsoft.com, www.cloudflare.com). И откройте порт инбаунда: ufw allow 443/tcp.
Шаг 3. Хост (Hosts)
Инбаунд есть, но клиент о нём не знает — нужен хост. В Hosts → Add Host: инбаунд VLESS Reality, адрес — IP ноды, порт 443, в Security Settings — SNI www.microsoft.com и fingerprint chrome.
Важно. Поле Security в хосте принимает лишь Inbound Default, none или tls — значения reality там нет. Для Reality берите Inbound Default: хост унаследует reality из инбаунда, а pbk и sid панель подставит сама. Ручной security: reality даст 422.
Шаг 4. Группа (Groups)
Чтобы инбаунд дошёл до пользователя, он должен быть в группе. В Groups создаём test-group и включаем в неё VLESS Reality.
Клиент и его подключение (Users)
Users → Create User: имя, при желании лимит и срок, и обязательно — группа test-group.
Пользователь появляется со статусом Active; в строке — иконки копирования подписки, ссылки и QR.
Клик по QR открывает страницу подписки: QR-код и список конфигураций (VLESS-Reality-node1 с адресом ноды). Именно это вы отдаёте клиенту — он сканирует QR или вставляет ссылку в v2rayNG / v2rayN / Hiddify.
Подписка отдаёт готовый vless:// со всеми параметрами Reality:
Ссылки мало — убедимся, что Reality реально притворяется. TLS-проба на порт ноды с именем «жертвы» должна вернуть настоящий сертификат Microsoft: для наблюдателя 443 на вашей ноде выглядит как сервер Microsoft.
На ноде бэкапить нечего, кроме ssl_cert.pem/ssl_key.pem. Помните: сертификат ноды лежит в двух местах — файл и копия в поле Certificate карточки; при замене (например, новый IP → новый SAN) правьте оба, иначе будет self-signed certificate.
Обновление — это перетягивание образов. Перед панелью снимите дамп: alembic-миграции накатываются автоматически и назад не откатываются.
cd /opt/pasarguard && docker compose pull pasarguard && docker compose up -d pasarguard
cd /opt/pg-node && docker compose pull node && docker compose up -d
Грабля №8 (и совет напоследок). Тег latest удобен, но лишает контроля над мажорными обновлениями с ломающими изменениями — на проде фиксируйте конкретный тег (ghcr.io/pasarguard/panel:v5.2.1). А базовый образ Caddy без Cloudflare-модуля — восьмая грабля: не забудьте именно сборку caddy-cloudflare. И держите открытым docker logs — в PasarGuard почти всё диагностируется там, а не в интерфейсе.
Вопрос к залу. Какой сайт-«жертву» для Reality вы держите на проде — крупный CDN, нейтральный зарубежный ресурс или собственный домен? И чем закрываете управляющий 62050: голым ufw по IP панели, WireGuard/приватной сетью между панелью и нодами или отдельным туннелем? Любопытно собрать разные схемы.
Вы можете написать сейчас и зарегистрироваться позже.
Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.
Примечание: Ваш пост будет проверен модератором, прежде чем станет видимым.
Классический сценарий для PasarGuard — форка Marzban — это не «одна коробка со всем сразу», а связка из двух машин: отдельная панель и отдельная нода, отдающая трафик. Именно так и стоит запускать прод: панель хранит пользователей и раздаёт подписки, а нода крутит Xray-ядро и принимает клиентов. Ниже — полный разбор такой раздельной установки, где вместо игрушечного Shadowsocks сразу поднимается VLESS + Reality с маскировкой под чужой сайт, а панель закрывается боевым TLS от Let's Encrypt.
Отдельный акцент — на восьми местах, где инсталляция ломается молча: интерфейс при этом не говорит ничего внятного, и почти любую поломку приходится ловить в
docker logs. Всё, что здесь описано, снято с реального стенда: панельv5.2.1, нодаv0.5.3, ядро Xray26.3.27. Настраивать будем преимущественно через веб-панель — со скриншотами конкретных экранов; в терминал уходим только за тем, что иначе не сделать.Как всё устроено
Панель и нода живут на разных серверах намеренно. Панель — это «мозг»: пользователи, ядра, хосты, подписки, статистика. Нода — «мышцы»: на ней работает Xray и держит клиентские соединения. Связывает их управляющий gRPC-канал под самоподписанным TLS.
Сколько ресурсов нужно
Обе машины стенда — почти одинаковые VPS. Цифры ниже сняты вживую; это рабочий минимум под одну ноду и несколько пользователей, без свопа.
Больше всех на панели ест не сама панель, а TimescaleDB: образ ~2 ГБ и несколько сотен мегабайт в покое. С SQLite панели хватило бы и 1 ГБ, но тогда пропадает статистика по нодам (об этом ниже). Под Timescale держите в уме 2 ГБ.
Порты
443 фигурирует дважды, но на разных хостах: на A его держит Caddy, на B — Xray. Пересечения нет.
База: почему Timescale, а не SQLite
SQLite в PasarGuard есть, и для одной локальной ноды его достаточно. Но выносите ноды на отдельные серверы и захотите графики по каждой — упрётесь в ограничение прямо из
.env.example:Запись статистики по нодам живёт только на PostgreSQL/TimescaleDB; на SQLite флаг мёртвый, и раздел статистики по узлам просто исчезает. Поэтому база — TimescaleDB (это PostgreSQL с расширением под временные ряды, что удобно для поминутного трафика).
Что приготовить заранее
Чтобы не спотыкаться посреди установки, соберите это до первой команды: два VPS с root и их публичные IP (
PANEL_IP,NODE_IP); домен для панели, делегированный на NS Cloudflare (DNS-01 ходит через её API); аккаунт Cloudflare и токен с правамиZone:DNSна зону (My Profile → API Tokens → Create Token, шаблон Edit zone DNS, в Zone Resources — ваша зона; значение показывается один раз); и проверьте, что нужные порты не закрыты у провайдера. A-запись на домен заведите сразу — пока ставится Docker, она успеет разойтись по DNS.Готовим обе машины
Docker ставим официальным скриптом — он сам подцепит репозиторий и compose-плагин. Одинаково для A и B:
Проверьте, что доступен именно
docker compose(v2-плагин через пробел) — на нём построен весь гайд.Сервер A: база и панель
Заводим каталог и один раз генерируем секреты — пароль БД и
api_key(общий секрет панели и ноды):Посмотреть значения в любой момент —
cat /opt/pasarguard/.secrets. Ниже по тексту они идут плейсхолдерами и подставляются руками:DB_PASS— в.envи в compose базы,API_KEY— позже на сервере B и в карточке ноды (забрать:grep API_KEY /opt/pasarguard/.secrets).PANEL_IPNODE_IPDB_PASSopenssl rand -hex 24на A.envпанели и compose базыAPI_KEY.secrets)Caddyfile,VITE_BASE_API,ALLOWED_ORIGINSCF_API_TOKENPRIVATE_KEY/PUBLIC_KEYdocker exec pg-node xray x25519shortIdopenssl rand -hex 8shortIds)Грабля №1: формат .env и падение --env-file
Оригинальный
.env.exampleидёт «с пробелами вокруг равно»:UVICORN_HOST = "0.0.0.0". Python-овскийdotenvэто переваривает, а докеровский--env-file— нет:Пробел утёк в имя переменной (
'UVICORN_HOST '). Лечится тем, что.envпишется без пробелов, строгоKEY=value. Полный файл с комментариями:Грабля №2: образы из Docker Hub и 429
В штатном compose образ —
pasarguard/panel:latest, то есть Hub, который на пуллах любит отдавать429 Too Many Requests. Берём зеркало ghcr:ghcr.io/pasarguard/panel:latestиghcr.io/pasarguard/node:latest. Compose сервера A:services: timescaledb: image: timescale/timescaledb:latest-pg17 container_name: pasarguard-db restart: always environment: POSTGRES_DB: pasarguard POSTGRES_USER: pasarguard POSTGRES_PASSWORD: DB_PASS volumes: - /var/lib/pasarguard-db/pgdata:/var/lib/postgresql/data # отдельно от состояния панели ports: - "127.0.0.1:5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U pasarguard -d pasarguard"] interval: 5s timeout: 5s retries: 20 pasarguard: image: ghcr.io/pasarguard/panel:v5.2.1 # версия со стенда container_name: pasarguard restart: always env_file: .env network_mode: host volumes: - /var/lib/pasarguard:/var/lib/pasarguard depends_on: timescaledb: condition: service_healthyПоднимаем по очереди — сначала БД до
healthy, затем панель:Грабля №3: без TLS панель слушает только localhost
После миграций в логе появится предупреждение — не пугайтесь, оно ожидаемо:
Это и есть задумка: панель на
127.0.0.1:8000, а TLS и внешний доступ обеспечит Caddy. Поэтому в.envи стоитUVICORN_HOST=127.0.0.1. Ниже будет иAttention: You have no node— ноду добавим позже.Открываем панель наружу: Caddy и TLS через Cloudflare DNS-01 (грабля №4)
Панель доступна только на localhost — ставим перед ней Caddy с сертификатом Let's Encrypt по DNS-01 (открытый 80 для ACME не нужен). И тут грабля: обычный
caddy:2-alpineне умеет Cloudflare — нужен билд с DNS-модулем,ghcr.io/caddybuilds/caddy-cloudflare:latest. В нём модульdns.providers.cloudflareуже вкомпилирован, и этот же образ идёт в compose ниже — отдельной команды под проверку не нужно: будь модуль не на месте, Caddy упал бы на directivedns cloudflareс понятной ошибкой.В зоне Cloudflare заводим
A-записьpg-test.gig.ovh → PANEL_IPв режиме DNS only (серое облако). Caddyfile — это reverse-proxy на панель плюс TLS через DNS-01; Caddy сам возьмёт настоящий, доверенный браузерами сертификат Let's Encrypt:{ email admin@gig.ovh } pg-test.gig.ovh { reverse_proxy 127.0.0.1:8000 tls { dns cloudflare {env.CF_API_TOKEN} resolvers 1.1.1.1 } }services: caddy: image: ghcr.io/caddybuilds/caddy-cloudflare:latest container_name: caddy restart: always network_mode: host environment: CF_API_TOKEN: "CLOUDFLARE_TOKEN" volumes: - /opt/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - /var/lib/caddy/data:/data - /var/lib/caddy/config:/configВо время челленджа в зоне на секунды появляется TXT
_acme-challenge.pg-test.gig.ovhи затем убирается — так работает DNS-01. Проверяем сертификат снаружи:Ключи для работы за прокси мы задали в
.envещё на старте, поэтому перезапускать панель не нужно — домен сразу отдаёт дашборд:curl -s -o /dev/null -w "%{http_code}\n" https://pg-test.gig.ovh/dashboard/ # 200Готово: панель открывается по
https://pg-test.gig.ovhс валидным сертификатом, дальше работаем в браузере. Опционально: при отладке, чтобы не жечь лимиты боевого Let's Encrypt, можно временно добавить в глобальный блок Caddyfile строкуacme_ca https://acme-staging-v02.api.letsencrypt.org/directory(сертификат будет недоверенный — это норма для проверки), затем убрать её, удалить/var/lib/caddy/data/caddy/certificatesи перезапустить Caddy.Грабля №5: владелец и мёртвый SUDO_USERNAME
Попытка войти статическим админом из
.envв 5.2.1 упирается в отлуп:В проде env-админ запрещён специально. Владелец создаётся одноразовым ключом через CLI, и тут вторая ловушка: команды
pasarguardвPATHнет,cli_wrapper.shне исполняемый. Работает только прямой вызов Python внутри контейнера (рабочий каталог/code):Ключ действует 5 минут и однократно. Панель уже открывается по домену — идём на
https://pg-test.gig.ovh/dashboard/, на логине жмём Owner access, режим Create и вводим ключ, логин и пароль владельца.После входа попадаем на дашборд — дальше почти всё мышкой.
Карта интерфейса: за что отвечает каждая вкладка
Меню слева — это весь PasarGuard целиком. Коротко по каждому пункту.
Дальше мы и пройдём по этой карте: подключим ноду (Nodes), настроим на ней протокол (Core Configs), выставим наружу (Hosts), соберём в тариф (Groups) и выдадим клиенту (Users).
Сервер B: нода и её сертификат
Грабля №6: без сертификата нода не стартует, а без SAN — не подключается
Канал панель↔нода шифруется TLS, и сертификат обязан существовать до первого запуска, иначе контейнер циклится:
Сделали сертификат «обычной» командой без SAN — панель отвергает ноду на подключении:
Панель сверяет именно SAN с IP, а не
CN. Правильная генерация добавляетsubjectAltName=IP:...:Compose ноды (образ из ghcr;
API_KEY— тот самый UUID с панели; нуженNET_ADMINи сеть хоста):services: node: image: ghcr.io/pasarguard/node:v0.5.3 # версия со стенда container_name: pg-node restart: always network_mode: host cap_add: - NET_ADMIN environment: SERVICE_PORT: 62050 SERVICE_PROTOCOL: "grpc" SSL_CERT_FILE: "/var/lib/pg-node/certs/ssl_cert.pem" SSL_KEY_FILE: "/var/lib/pg-node/certs/ssl_key.pem" API_KEY: "API_KEY" GENERATED_CONFIG_PATH: "/var/lib/pg-node/generated" volumes: - /var/lib/pg-node:/var/lib/pg-nodeСтроку
Failed to load env file ... open .envигнорируем — переменные пришли черезenvironment:.Файрвол ноды
Управляющий 62050 открываем только для IP панели, SSH разрешаем до включения ufw:
Политика ufw по умолчанию —
deny incoming, поэтому отдельное запрещающее правило для 62050 не нужно: всё, что не разрешено явно, отбрасывается ещё на SYN.Подключаем ноду в панели (Nodes)
В Nodes → Nodes жмём Create Node. Форма короткая, но одно поле коварно — и при ошибке причина видна только в
docker logs pasarguard, тогда как в UI просто краснеет точка.Грабля №7: не тот сертификат в карточке
В поле Certificate (в API —
server_ca) кладут сертификат с ноды целиком. Подсунете чужой (скажем, свежесгенерированный на панели) — отвал, и причина снова только в логе:Сертификат берём так:
cat /var/lib/pg-node/certs/ssl_cert.pem. Успех в логе панели:А в списке нод точка зеленеет, появляются версии ноды (0.5.3) и ядра (26.3.27):
Красный статус? Не гадайте по UI — открывайте
docker logs pasarguard.VLESS + Reality: узел, который выглядит как чужой сайт
Дефолтный Shadowsocks хорош лишь для проверки «а оно вообще заводится». Боевой протокол на Xray сегодня — VLESS + Reality. Суть: нода не поднимает собственный TLS (который сам по себе выдаёт прокси), а на рукопожатии подделывается под чужой сайт, заимствуя TLS у реального внешнего хоста (например,
www.microsoft.com). Снаружи 443 на ноде неотличим от настоящего Microsoft: тот же сертификат, та же цепочка. Своего домена и сертификата для клиента не требуется.Шаг 1. Ключи Reality
Reality опирается на пару x25519 и короткий
shortId. Ключи выдаёт само ядро прямо в контейнере ноды:PrivateKey— в конфиг инбаунда на ноде,PublicKey(pbk) панель сама подставит в клиентскую ссылку. Сохраните обе половины.Шаг 2. Инбаунд в Core Config
В Nodes → Core Configs открываем
Default Core Config, на вкладке Inbounds жмём Add inbound: типvless, порт443, securityreality. Проще всего вставить готовый JSON (через{}-редактор), заменив плейсхолдеры своими ключами:{ "tag": "VLESS Reality", "listen": "0.0.0.0", "port": 443, "protocol": "vless", "settings": { "clients": [], "decryption": "none" }, "streamSettings": { "network": "tcp", "security": "reality", "realitySettings": { "show": false, "dest": "www.microsoft.com:443", "xver": 0, "serverNames": ["www.microsoft.com"], "privateKey": "<PRIVATE_KEY>", "shortIds": ["86d173896823210b"] } }, "sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"] } }Оставляем галку Restart Nodes и сохраняем — панель раскатает конфиг на ноду и перезапустит ядро. В списке появляется
VLESS Reality / vless / 443:Шаг 3. Хост (Hosts)
Инбаунд есть, но клиент о нём не знает — нужен хост. В Hosts → Add Host: инбаунд
VLESS Reality, адрес — IP ноды, порт443, в Security Settings — SNIwww.microsoft.comи fingerprintchrome.Шаг 4. Группа (Groups)
Чтобы инбаунд дошёл до пользователя, он должен быть в группе. В Groups создаём
test-groupи включаем в неёVLESS Reality.Клиент и его подключение (Users)
Users → Create User: имя, при желании лимит и срок, и обязательно — группа
test-group.Пользователь появляется со статусом Active; в строке — иконки копирования подписки, ссылки и QR.
Клик по QR открывает страницу подписки: QR-код и список конфигураций (
VLESS-Reality-node1с адресом ноды). Именно это вы отдаёте клиенту — он сканирует QR или вставляет ссылку в v2rayNG / v2rayN / Hiddify.Подписка отдаёт готовый
vless://со всеми параметрами Reality:vless://<UUID>@NODE_IP:443?encryption=none&security=reality&type=tcp&headerType=none &sni=www.microsoft.com&fp=chrome&pbk=<PUBLIC_KEY>&sid=86d173896823210b#VLESS-Reality-node1Проверка, что маскировка живая
Ссылки мало — убедимся, что Reality реально притворяется. TLS-проба на порт ноды с именем «жертвы» должна вернуть настоящий сертификат Microsoft: для наблюдателя 443 на вашей ноде выглядит как сервер Microsoft.
Цепочка Microsoft — значит Reality поднят и корректно одалживает чужой TLS. Импортируйте
vless://в клиент и проверьте реальный выход.Устойчивость к перезапуску
Проверим, что нода сама возвращается после рестарта всего стека:
Через полминуты-минуту статус снова
connected, а в логе панели — свежая параConnecting to ... / Connected to. Руками ничего делать не нужно.Если не завелось: симптом → где смотреть
docker logs caddydocker logs pasarguardVITE_BASE_APIdocker logs pasarguard-dbDB_PASSили он разъехался с composedocker logs pg-node/var/lib/pg-node/certsdocker logs pasarguardapi_key, закрыт 62050docker logs pg-node+ Statisticspbk/sid/sniБэкап и обновление
Состояние панели — база и
/var/lib/pasarguard:На ноде бэкапить нечего, кроме
ssl_cert.pem/ssl_key.pem. Помните: сертификат ноды лежит в двух местах — файл и копия в поле Certificate карточки; при замене (например, новый IP → новый SAN) правьте оба, иначе будетself-signed certificate.Обновление — это перетягивание образов. Перед панелью снимите дамп: alembic-миграции накатываются автоматически и назад не откатываются.
Источники
TOP HOSTERS: KAMATERA (30 дней бесплатного теста!)
Универсальный хостер №1 - 4VPS.su (2Гб\с сервера) - 10% скидка на первый заказ или 15% бонус на первое пополнение