240 lines
9.0 KiB
Markdown
240 lines
9.0 KiB
Markdown
# VPN Panel
|
||
|
||
Админ-панель VPN на Go + PostgreSQL 17. Готово к деплою в **Dokploy** через Docker Compose.
|
||
|
||
## Стек
|
||
|
||
- Go 1.23 (веб-админка)
|
||
- PostgreSQL 17
|
||
- Docker Compose
|
||
- Reverse proxy + SSL: **Caddy** / **Nginx** / **Traefik**
|
||
|
||
## Быстрый старт (Dokploy / Docker)
|
||
|
||
1. В Dokploy создайте **Docker Compose** приложение из этого репозитория.
|
||
2. Задайте Environment Variables (`.env` в git не лежит):
|
||
|
||
```env
|
||
APP_SECRET=long-random-secret
|
||
ADMIN_EMAIL=admin@example.com
|
||
ADMIN_PASSWORD=strong-password
|
||
POSTGRES_PASSWORD=strong-db-password
|
||
```
|
||
|
||
3. Deploy. Домен в Dokploy → сервис **`app`**, порт **`8080`** (не postgres).
|
||
4. Если **Bad Gateway**: смотрите логи `app` — чаще всего падение из‑за БД или домен смотрит не на тот порт.
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
docker compose logs -f app
|
||
```
|
||
|
||
Панель: `http://localhost:8080` · health: `/health`
|
||
|
||
## Авторизация админа (из `.env`)
|
||
|
||
| Переменная | Значение по умолчанию |
|
||
|---|---|
|
||
| `ADMIN_EMAIL` | `admin@panel.local` |
|
||
| `ADMIN_PASSWORD` | `Admin123!ChangeMe` |
|
||
| `ADMIN_NAME` | `Administrator` |
|
||
|
||
При первом запуске админ создаётся автоматически из этих переменных.
|
||
|
||
---
|
||
|
||
## Режим ноды (как Remnawave, но проще)
|
||
|
||
Панель **не** гоняет Xray сама — VPN поднимается на отдельных серверах-нодах (как [Remnawave Node](https://docs.rw/install/remnawave-node)).
|
||
|
||
### Чем удобнее
|
||
|
||
| Remnawave Node | VPN Panel |
|
||
|---|---|
|
||
| Docker вручную + вставить compose | **Авто по SSH**: host/user/пароль (или ключ) → панель ставит Docker + агент |
|
||
| Или вручную | **Ручной режим** — тот же compose, если SSH недоступен |
|
||
|
||
### Как добавить ноду
|
||
|
||
1. Админка → **Ноды** → **Добавить ноду**
|
||
2. Укажите имя, IP/хост, SSH (`root` + пароль или ключ), порт API ноды (по умолчанию `2222`)
|
||
3. Режим:
|
||
- **Авто по SSH** — мастер подключается, ставит Docker (если нет), заливает бинарник `vpn-node`, поднимает `docker compose`
|
||
- **Вручную** — скопируйте `docker-compose.yml` и бинарник `/app/bin/vpn-node` на сервер
|
||
4. На firewall ноды откройте `NODE_PORT` **только** для IP панели
|
||
5. Нажмите **Проверить online**
|
||
|
||
Агент слушает `NODE_PORT`, авторизация: `Authorization: Bearer <SECRET_KEY>`.
|
||
|
||
Эндпоинты агента: `GET /health`, `GET|POST /api/v1/config`, `GET /api/v1/status`.
|
||
|
||
SSH-секреты в БД шифруются AES-GCM ключом от `APP_SECRET`.
|
||
|
||
> Панель в Docker должна иметь сетевой доступ до SSH (22) и NODE_PORT ноды. На VPS/Dokploy это обычно ок.
|
||
|
||
---
|
||
|
||
## Reverse proxy + SSL (Let's Encrypt)
|
||
|
||
Примеры лежат в `deploy/`. Сеть Docker: `vpn-panel-net`.
|
||
|
||
### Что нужно до выпуска сертификата
|
||
|
||
1. **DNS**: A (и при необходимости AAAA) запись `DOMAIN` → публичный IP сервера.
|
||
2. **Порты**: `80` и `443` открыты на firewall / security group (TCP; для Caddy HTTP/3 ещё `443/udp`).
|
||
3. **Домен в `.env`**:
|
||
```env
|
||
DOMAIN=panel.example.com
|
||
ACME_EMAIL=admin@example.com
|
||
```
|
||
4. **Не светить панель наружу на `:8080`** — только через proxy:
|
||
```bash
|
||
docker compose -f docker-compose.yml -f deploy/compose.behind-proxy.yml up -d
|
||
```
|
||
Либо в Dokploy / compose закомментируйте `ports` у `app`, оставьте доступ по внутренней сети `app:8080`.
|
||
5. Прокси обязан передавать заголовки `Host`, `X-Forwarded-Proto`, `X-Forwarded-For` (уже есть в примерах).
|
||
|
||
> Let's Encrypt не выпустит сертификат на `localhost` / приватный IP. Нужен реальный домен с публичным DNS.
|
||
|
||
---
|
||
|
||
### Вариант A — Caddy (самый простой, SSL автоматом)
|
||
|
||
Caddy сам получает и продлевает сертификат.
|
||
|
||
1. В `deploy/caddy/Caddyfile` домен берётся из `{$DOMAIN:...}` (или задайте в `.env`).
|
||
2. Запуск вместе с панелью:
|
||
|
||
```bash
|
||
# из корня репозитория
|
||
docker compose \
|
||
-f docker-compose.yml \
|
||
-f deploy/compose.behind-proxy.yml \
|
||
-f deploy/caddy/docker-compose.caddy.yml \
|
||
up -d --build
|
||
```
|
||
|
||
3. Откройте `https://panel.example.com`.
|
||
|
||
Файлы: `deploy/caddy/Caddyfile`, `deploy/caddy/docker-compose.caddy.yml`.
|
||
|
||
---
|
||
|
||
### Вариант B — Nginx + Certbot
|
||
|
||
1. Подставьте свой домен в `deploy/nginx/nginx.conf` (`server_name` и пути к сертификатам).
|
||
2. Поднимите стек (сначала HTTP + ACME webroot):
|
||
|
||
```bash
|
||
docker compose \
|
||
-f docker-compose.yml \
|
||
-f deploy/compose.behind-proxy.yml \
|
||
-f deploy/nginx/docker-compose.nginx.yml \
|
||
up -d --build
|
||
```
|
||
|
||
3. Выпустите сертификат:
|
||
|
||
```bash
|
||
docker compose -f deploy/nginx/docker-compose.nginx.yml run --rm certbot certonly \
|
||
--webroot -w /var/www/certbot \
|
||
-d panel.example.com \
|
||
--email admin@example.com \
|
||
--agree-tos --no-eff-email
|
||
```
|
||
|
||
4. В `deploy/nginx/nginx.conf`:
|
||
- раскомментируйте блок `server { listen 443 ... }`;
|
||
- включите `return 301 https://$host$request_uri;` в HTTP-сервере;
|
||
- закомментируйте временный `location /` на порту 80 (оставьте только ACME).
|
||
5. Перезагрузите nginx:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.yml -f deploy/nginx/docker-compose.nginx.yml exec nginx nginx -s reload
|
||
```
|
||
|
||
Продление: контейнер `certbot` крутит `renew` раз в 12 часов; после renew — `nginx -s reload`.
|
||
|
||
Файлы: `deploy/nginx/`.
|
||
|
||
---
|
||
|
||
### Вариант C — Traefik (свой или Dokploy)
|
||
|
||
#### C1. Свой Traefik из репозитория
|
||
|
||
```bash
|
||
docker compose \
|
||
-f docker-compose.yml \
|
||
-f deploy/compose.behind-proxy.yml \
|
||
-f deploy/traefik/docker-compose.traefik.yml \
|
||
up -d --build
|
||
```
|
||
|
||
В `.env`: `DOMAIN`, `ACME_EMAIL`. Сертификат выпускается HTTP-challenge на entrypoint `web`.
|
||
|
||
#### C2. Dokploy (встроенный Traefik)
|
||
|
||
Dokploy уже поднимает Traefik — **не** добавляйте `deploy/traefik/docker-compose.traefik.yml`.
|
||
|
||
1. В Dokploy укажите домен для сервиса `app`.
|
||
2. Либо добавьте labels к `app` (см. `deploy/traefik/labels.example.yml`):
|
||
|
||
```yaml
|
||
labels:
|
||
- traefik.enable=true
|
||
- traefik.http.routers.vpn-panel.rule=Host(`panel.example.com`)
|
||
- traefik.http.routers.vpn-panel.entrypoints=websecure
|
||
- traefik.http.routers.vpn-panel.tls=true
|
||
- traefik.http.routers.vpn-panel.tls.certresolver=letsencrypt
|
||
- traefik.http.services.vpn-panel.loadbalancer.server.port=8080
|
||
```
|
||
|
||
Имя `certresolver` в Dokploy часто `letsencrypt` — проверьте в настройках (или `TRAEFIK_CERT_RESOLVER` в `.env`).
|
||
|
||
3. Не публикуйте порт `8080` наружу в UI Dokploy, если трафик идёт только через Traefik.
|
||
|
||
---
|
||
|
||
### Чеклист после включения HTTPS
|
||
|
||
| Проверка | Ожидание |
|
||
|---|---|
|
||
| `https://DOMAIN/` | Главная панель |
|
||
| `https://DOMAIN/health` | `ok` |
|
||
| HTTP → HTTPS | Редирект 301 (Caddy/Traefik по умолчанию; Nginx — после шага 4) |
|
||
| Сертификат | Валидный LE, не self-signed |
|
||
| Порт `8080` снаружи | Закрыт |
|
||
|
||
### Типичные ошибки
|
||
|
||
| Симптом | Причина |
|
||
|---|---|
|
||
| ACME timeout / pending | DNS ещё не указывает на сервер, или закрыт порт 80 |
|
||
| 502 Bad Gateway | Proxy не в сети `vpn-panel-net` / сервис `app` не запущен |
|
||
| Редирект-петля | За приложением HTTPS, а `X-Forwarded-Proto` не передаётся |
|
||
| Certbot: no such file | Домен в путях `nginx.conf` не совпадает с `-d` |
|
||
|
||
---
|
||
|
||
## Протоколы (seed)
|
||
|
||
WireGuard, OpenVPN, VLESS, VMess, Trojan, Shadowsocks, Hysteria2 — можно включать/выключать в админке.
|
||
|
||
## Локальная разработка без Docker
|
||
|
||
```bash
|
||
# нужен локальный Postgres 17 и .env с POSTGRES_HOST=localhost
|
||
go mod tidy
|
||
go run ./cmd/server
|
||
```
|
||
|
||
## Эндпоинты
|
||
|
||
- `/` — главная
|
||
- `/login` — вход
|
||
- `/admin` — дашборд
|
||
- `/admin/nodes` — ноды
|
||
- `/admin/protocols` — протоколы
|
||
- `/health` — healthcheck
|