docs(README): добавлены инструкции по локальному запуску и развертыванию с Traefik
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 46s
Build and Push Auth Portal Docker Image / create-release (push) Skipped

This commit is contained in:
Denozordec
2026-07-18 18:56:37 +07:00
parent a4ece3d344
commit 638df5d679
3 changed files with 442 additions and 0 deletions
+5
View File
@@ -52,10 +52,15 @@ pnpm dlx shadcn@latest add @reui/auth-13 --yes
- Secrets: `ACTIONS_PAT`, `GITEA_TOKEN`
- Образ: `git.shts.su/denozord/auth-portal`
Локально / без reverse proxy:
```bash
docker compose up -d --build
```
**Production за Traefik** (удалённый сервер, Compose или `docker run`, HTTPS через Cloudflare DNS challenge):
[`docs/deploy-traefik.md`](docs/deploy-traefik.md) · пример [`deploy/docker-compose.traefik.yml`](deploy/docker-compose.traefik.yml)
## Структура
```
+67
View File
@@ -0,0 +1,67 @@
# Auth Portal behind an existing Traefik (production).
# Docs: docs/deploy-traefik.md
#
# Usage on server:
# cp deploy/docker-compose.traefik.yml /opt/auth-portal/docker-compose.yml
# # edit networks.proxy.name if your Traefik network is not "proxy"
# docker compose --env-file .env up -d
services:
app:
image: git.shts.su/denozord/auth-portal:${AUTH_IMAGE_TAG:-latest}
pull_policy: always
container_name: auth-portal
restart: unless-stopped
# Do not publish ports — Traefik reaches the container on the shared network.
# ports:
# - "8080:8080"
env_file:
- .env
environment:
DATABASE_URL: sqlite:/data/app.db
STATIC_DIR: /app/static
NODE_ENV: production
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
JWT_TTL_HOURS: ${JWT_TTL_HOURS:-1}
REFRESH_TTL_DAYS: ${REFRESH_TTL_DAYS:-14}
ISSUER: ${ISSUER:-https://auth.shnt.top}
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@shnt.top}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?set ADMIN_PASSWORD in .env}
ADMIN_NAME: ${ADMIN_NAME:-Admin}
RETURN_TO_ALLOWLIST: ${RETURN_TO_ALLOWLIST:-.shnt.top}
LOG_LEVEL: ${LOG_LEVEL:-info}
volumes:
- ./data:/data
networks:
- proxy
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.http.routers.auth-portal.rule=Host(`${AUTH_DOMAIN:-auth.shnt.top}`)
- traefik.http.routers.auth-portal.entrypoints=websecure
- traefik.http.routers.auth-portal.tls=true
- traefik.http.routers.auth-portal.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}
- traefik.http.services.auth-portal.loadbalancer.server.port=8080
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
proxy:
external: true
# Rename if Traefik uses another network (traefik, web, edge, …):
name: proxy
+370
View File
@@ -0,0 +1,370 @@
# Развёртывание Auth Portal за Traefik
Production: один контейнер (API + статика SPA), SQLite в томе, HTTPS через Traefik + Let's Encrypt (**DNS-01 challenge через Cloudflare**).
Образ: `git.shts.su/denozord/auth-portal` (CI: `.gitea/workflows/docker.yml`).
Публичный URL по умолчанию: `https://auth.shnt.top`.
Документация:
- Traefik Docker / TLS: [Expose Docker](https://doc.traefik.io/traefik/expose/docker/basic/), [ACME](https://doc.traefik.io/traefik/https/acme/)
- Cloudflare как DNS provider для Lego/Traefik: env `CF_DNS_API_TOKEN`
## Предпосылки
1. Зона `shnt.top` (или ваш домен) в Cloudflare.
2. Docker на сервере; сеть для Traefik (часто `proxy`).
3. Доступ к registry: `docker login git.shts.su`.
4. Каталоги:
```bash
mkdir -p /opt/traefik /opt/auth-portal/data
```
---
## HTTPS: DNS challenge Cloudflare
HTTP-01 на порту 80 не нужен: Traefik создаёт TXT `_acme-challenge…` в Cloudflare и получает сертификат Let's Encrypt. Удобно, если порт 80 закрыт, домен за CF proxy, или нужен wildcard.
### 1. API-токен Cloudflare
[Cloudflare Dashboard → My Profile → API Tokens → Create Token](https://dash.cloudflare.com/profile/api-tokens)
Шаблон **Edit zone DNS** или Custom:
| Permission | Access |
|------------|--------|
| Zone → DNS | Edit |
| Zone → Zone | Read (желательно) |
**Zone Resources:** Include → Specific zone → `shnt.top` (минимальный scope).
Скопируйте токен один раз → это `CF_DNS_API_TOKEN`.
Не путать с Global API Key и не класть токен в репозиторий.
### 2. DNS-запись для портала
В Cloudflare → DNS → Records:
| Type | Name | Content | Proxy |
|------|------|---------|-------|
| `A` (или `AAAA`) | `auth` | публичный IP сервера | **DNS only** (серое облако) |
Для origin за Traefik на VPS обычно **DNS only**. Orange cloud (proxied) возможен, но тогда SSL/режимы CF настраиваются отдельно; ACME DNS-01 от этого не зависит.
Проверка:
```bash
dig +short auth.shnt.top A
```
### 3. Traefik с `dnschallenge.provider=cloudflare`
Если Traefik уже настроен так же — переходите к [разделу Auth Portal](#вариант-a--docker-compose).
Если нет — пример `/opt/traefik/docker-compose.yml`:
```yaml
services:
traefik:
image: traefik:v3.7
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "80:80"
- "443:443"
environment:
CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN:?set CF_DNS_API_TOKEN}
# опционально: CF_ZONE_API_TOKEN с Zone:Read, если DNS-токен без Zone:Read
command:
- --log.level=INFO
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=proxy
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL:?set email}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.dnschallenge=true
- --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=15
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_letsencrypt:/letsencrypt
networks:
- proxy
volumes:
traefik_letsencrypt:
name: traefik_letsencrypt
networks:
proxy:
name: proxy
```
Файл `/opt/traefik/.env`:
```env
CF_DNS_API_TOKEN=<токен из шага 1>
LETSENCRYPT_EMAIL=admin@shnt.top
```
Запуск Traefik:
```bash
cd /opt/traefik
docker network create proxy 2>/dev/null || true
docker compose up -d
docker compose logs -f --tail=50
```
Имя resolver'а в команде — `letsencrypt`. Его же указывают приложения в label:
`traefik.http.routers.…tls.certresolver=letsencrypt`
**Важно:** `CF_DNS_API_TOKEN` задаётся в **окружении контейнера Traefik**, не auth-portal. Том `traefik_letsencrypt` хранит `acme.json` — не удаляйте (`docker compose down -v`) без бэкапа.
Проверка сети:
```bash
docker network ls
docker inspect traefik --format '{{json .NetworkSettings.Networks}}'
```
---
## Переменные окружения Auth Portal
Файл `/opt/auth-portal/.env` (не коммитить):
```env
# Обязательно: общий секрет с VPS Tracker / CFDM (AUTH_JWT_SECRET)
JWT_SECRET=<длинная-случайная-строка>
ISSUER=https://auth.shnt.top
JWT_TTL_HOURS=1
REFRESH_TTL_DAYS=14
# Bootstrap admin — только при пустой БД
ADMIN_EMAIL=admin@shnt.top
ADMIN_PASSWORD=<сильный-пароль>
ADMIN_NAME=Admin
# Origins приложений (SSO return_to). Доменный суффикс .shnt.top покрывает поддомены.
RETURN_TO_ALLOWLIST=.shnt.top,https://vps.shnt.top,https://cfdm.shnt.top
DATABASE_URL=sqlite:/data/app.db
STATIC_DIR=/app/static
LOG_LEVEL=info
NODE_ENV=production
# Публичный хост (для compose-labels) + имя ACME resolver Traefik
AUTH_DOMAIN=auth.shnt.top
TRAEFIK_CERTRESOLVER=letsencrypt
```
| Переменная | Назначение |
|------------|------------|
| `JWT_SECRET` | HS256 для access JWT; тот же секрет в приложениях |
| `ISSUER` | Должен совпадать с `AUTH_ISSUER` в приложениях |
| `RETURN_TO_ALLOWLIST` | Иначе SSO `return_to` отклоняется |
| `ADMIN_*` | Первый админ при пустом `/data/app.db` |
| `TRAEFIK_CERTRESOLVER` | Имя resolver'а Traefik (`letsencrypt`) |
В production cookie refresh ставится с флагом `Secure` — нужен HTTPS (Traefik).
---
## Вариант A — Docker Compose
Готовый файл в репозитории: [`deploy/docker-compose.traefik.yml`](../deploy/docker-compose.traefik.yml).
На сервере:
```bash
cd /opt/auth-portal
# скопируйте deploy/docker-compose.traefik.yml → docker-compose.yml
# или:
curl -fsSL -o docker-compose.yml \
https://git.shts.su/denozord/auth-portal/raw/branch/main/deploy/docker-compose.traefik.yml
# поправьте networks.proxy.name под вашу сеть Traefik
nano docker-compose.yml
nano .env
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f --tail=100
```
После старта Traefik запросит сертификат (DNS TXT в Cloudflare). Первая выдача может занять 30–90 с.
Проверка:
```bash
curl -fsS https://auth.shnt.top/health
echo | openssl s_client -connect auth.shnt.top:443 -servername auth.shnt.top 2>/dev/null | openssl x509 -noout -issuer -dates -subject
```
Обновление образа:
```bash
cd /opt/auth-portal
docker compose pull
docker compose up -d
```
Остановка:
```bash
docker compose down
# данные в ./data сохраняются (не используйте -v без бэкапа)
```
---
## Вариант B — Docker CLI
Подставьте имя сети Traefik вместо `proxy`, если нужно.
```bash
cd /opt/auth-portal
set -a && source .env && set +a
docker pull git.shts.su/denozord/auth-portal:latest
docker rm -f auth-portal 2>/dev/null || true
DOMAIN="${AUTH_DOMAIN:-auth.shnt.top}"
NETWORK=proxy # имя сети Traefik
RESOLVER="${TRAEFIK_CERTRESOLVER:-letsencrypt}"
docker run -d \
--name auth-portal \
--restart unless-stopped \
--network "$NETWORK" \
--env-file /opt/auth-portal/.env \
-v /opt/auth-portal/data:/data \
-l traefik.enable=true \
-l "traefik.docker.network=${NETWORK}" \
-l "traefik.http.routers.auth-portal.rule=Host(\`${DOMAIN}\`)" \
-l traefik.http.routers.auth-portal.entrypoints=websecure \
-l traefik.http.routers.auth-portal.tls=true \
-l "traefik.http.routers.auth-portal.tls.certresolver=${RESOLVER}" \
-l traefik.http.services.auth-portal.loadbalancer.server.port=8080 \
git.shts.su/denozord/auth-portal:latest
```
Порты хоста (`-p 8080:8080`) **не публикуйте**, если трафик только через Traefik.
Обновление:
```bash
docker pull git.shts.su/denozord/auth-portal:latest
docker stop auth-portal
docker rm auth-portal
# повторите docker run …
```
Логи / health:
```bash
docker logs -f --tail=100 auth-portal
docker logs -f --tail=100 traefik
docker inspect --format='{{.State.Health.Status}}' auth-portal
curl -fsS https://auth.shnt.top/health
```
---
## Labels Traefik (справка)
| Label | Значение |
|-------|----------|
| `traefik.enable` | `true` |
| `traefik.docker.network` | имя общей сети с Traefik |
| `…routers.auth-portal.rule` | `Host(\`auth.shnt.top\`)` |
| `…entrypoints` | `websecure` |
| `…tls` / `…tls.certresolver` | `true` / `letsencrypt` (имя из ACME resolver) |
| `…loadbalancer.server.port` | `8080` (порт внутри контейнера) |
Если resolver называется иначе (`le`, `cf`), замените `certresolver=…` и `TRAEFIK_CERTRESOLVER`.
---
## Связка с приложениями
После деплоя портала в VPS Tracker / CFDM:
```env
AUTH_REQUIRED=true
AUTH_JWT_SECRET=<тот же JWT_SECRET>
AUTH_ISSUER=https://auth.shnt.top
AUTH_PORTAL_URL=https://auth.shnt.top
```
UI:
```env
VITE_AUTH_ENABLED=true
VITE_AUTH_PORTAL_URL=https://auth.shnt.top
```
Интеграции: [integrate-vps-tracker.md](integrate-vps-tracker.md), [integrate-cfdm.md](integrate-cfdm.md).
Logout SSO: приложения редиректят на `https://auth.shnt.top/logout` (не на `/?return_to=…`).
---
## Бэкап и откат
```bash
# бэкап SQLite
cp /opt/auth-portal/data/app.db /opt/auth-portal/data/app.db.bak-$(date +%F)
# бэкап ACME (сертификаты Traefik)
docker run --rm -v traefik_letsencrypt:/data -v "$PWD:/backup" alpine \
tar czf /backup/traefik-acme-$(date +%F).tgz -C /data .
# откат на тег релиза
docker pull git.shts.su/denozord/auth-portal:vX.Y.Z
# в compose: image: …:vX.Y.Z → up -d
```
Не удаляйте том/каталог `data` и том `traefik_letsencrypt` без необходимости.
---
## Troubleshooting
| Симптом | Что проверить |
|---------|----------------|
| 404 / Gateway Timeout | Контейнер в той же сети, что Traefik; `traefik.docker.network`; label `loadbalancer.server.port=8080` |
| ACME / TLS не выдаётся | `CF_DNS_API_TOKEN` в env **Traefik**; права `Zone:DNS:Edit`; `dnschallenge.provider=cloudflare`; логи `docker logs traefik` на `error` / `acme` |
| `invalid credentials` / Cloudflare API | Токен не Global Key; зона в scope токена; нет лишних пробелов/кавычек в `.env` |
| TXT не появляется | Токен без Edit на зоне; неверная зона (другой аккаунт CF) |
| Сертификат есть, сайт не открывается | `A` на IP сервера; firewall 443; record не указывает на старый IP |
| Login OK, SSO в app падает | `JWT_SECRET` / `ISSUER` совпадают; app в JWT `apps` |
| `return_to` rejected | origin приложения в `RETURN_TO_ALLOWLIST` |
| Cookie не держится | HTTPS; `NODE_ENV=production` → `Secure` |
| «Выйти» возвращает в app | Нужен образ портала с `/logout` и клиенты с `redirectToPortalLogout` |
| Пустой UI / 404 статики | В образе `STATIC_DIR=/app/static` (зашито в Dockerfile) |
Полезные логи ACME:
```bash
docker logs traefik 2>&1 | grep -iE 'acme|certificate|cloudflare|error'
```
Локальный smoke без Traefik (порт наружу):
```bash
docker run --rm -p 8080:8080 --env-file .env -v "$PWD/data:/data" \
git.shts.su/denozord/auth-portal:latest
curl -fsS http://127.0.0.1:8080/health
```