diff --git a/README.md b/README.md index 5176fbc..41cd20a 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,8 @@ pnpm dlx shadcn@latest add @reui/auth-13 --yes 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) +**Production (один Compose: Traefik + auth-portal, HTTPS Cloudflare DNS challenge):** +[`docs/deploy-traefik.md`](docs/deploy-traefik.md) · [`deploy/docker-compose.traefik.yml`](deploy/docker-compose.traefik.yml) · [`deploy/env.traefik.example`](deploy/env.traefik.example) ## Структура diff --git a/deploy/docker-compose.traefik.yml b/deploy/docker-compose.traefik.yml index d1a61a3..aca0d87 100644 --- a/deploy/docker-compose.traefik.yml +++ b/deploy/docker-compose.traefik.yml @@ -1,20 +1,61 @@ -# Auth Portal behind an existing Traefik (production). +# Auth Portal + Traefik in one Compose stack (production). # Docs: docs/deploy-traefik.md # -# Usage on server: +# On server: +# mkdir -p /opt/auth-portal/data # 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 +# cp deploy/env.traefik.example /opt/auth-portal/.env # fill secrets +# docker login git.shts.su +# cd /opt/auth-portal && docker compose pull && docker compose up -d services: + traefik: + image: traefik:${TRAEFIK_IMAGE_TAG:-v3.7} + container_name: auth-portal-traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + ports: + - "${TRAEFIK_HTTP_PORT:-80}:80" + - "${TRAEFIK_HTTPS_PORT:-443}:443" + environment: + CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN:?set CF_DNS_API_TOKEN in .env} + # Optional if DNS token lacks Zone:Read: + # CF_ZONE_API_TOKEN: ${CF_ZONE_API_TOKEN:-} + command: + - --log.level=${TRAEFIK_LOG_LEVEL:-INFO} + - --api.dashboard=false + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --providers.docker.network=auth-portal + - --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 LETSENCRYPT_EMAIL in .env} + - --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: + - auth-portal + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + 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" + depends_on: + - traefik + # No host ports — only Traefik publishes 80/443. env_file: - .env environment: @@ -33,14 +74,14 @@ services: volumes: - ./data:/data networks: - - proxy + - auth-portal labels: - traefik.enable=true - - traefik.docker.network=proxy + - traefik.docker.network=auth-portal - 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.routers.auth-portal.tls.certresolver=letsencrypt - traefik.http.services.auth-portal.loadbalancer.server.port=8080 healthcheck: test: @@ -60,8 +101,10 @@ services: max-size: "10m" max-file: "3" +volumes: + traefik_letsencrypt: + name: auth_portal_traefik_letsencrypt + networks: - proxy: - external: true - # Rename if Traefik uses another network (traefik, web, edge, …): - name: proxy + auth-portal: + name: auth-portal diff --git a/deploy/env.traefik.example b/deploy/env.traefik.example new file mode 100644 index 0000000..59637e5 --- /dev/null +++ b/deploy/env.traefik.example @@ -0,0 +1,24 @@ +# Production .env for deploy/docker-compose.traefik.yml (auth + Traefik). +# Copy to /opt/auth-portal/.env and fill secrets. Do not commit. + +# --- Traefik / Let's Encrypt (Cloudflare DNS-01) --- +CF_DNS_API_TOKEN= +LETSENCRYPT_EMAIL=admin@shnt.top +AUTH_DOMAIN=auth.shnt.top +# TRAEFIK_IMAGE_TAG=v3.7 +# TRAEFIK_HTTP_PORT=80 +# TRAEFIK_HTTPS_PORT=443 +# TRAEFIK_LOG_LEVEL=INFO + +# --- Auth Portal --- +AUTH_IMAGE_TAG=latest +JWT_SECRET= +JWT_TTL_HOURS=1 +REFRESH_TTL_DAYS=14 +ISSUER=https://auth.shnt.top +ADMIN_EMAIL=admin@shnt.top +ADMIN_PASSWORD= +ADMIN_NAME=Admin +RETURN_TO_ALLOWLIST=.shnt.top,https://vps.shnt.top,https://cfdm.shnt.top +LOG_LEVEL=info +NODE_ENV=production diff --git a/docs/deploy-traefik.md b/docs/deploy-traefik.md index a4bf4b3..0c639a3 100644 --- a/docs/deploy-traefik.md +++ b/docs/deploy-traefik.md @@ -1,306 +1,154 @@ -# Развёртывание Auth Portal за Traefik +# Развёртывание Auth Portal + Traefik (один Docker Compose) -Production: один контейнер (API + статика SPA), SQLite в томе, HTTPS через Traefik + Let's Encrypt (**DNS-01 challenge через Cloudflare**). +Один стек: **Traefik** (HTTPS, Let's Encrypt DNS-01 через Cloudflare) + **auth-portal** (API + SPA, SQLite). -Образ: `git.shts.su/denozord/auth-portal` (CI: `.gitea/workflows/docker.yml`). -Публичный URL по умолчанию: `https://auth.shnt.top`. +Образ приложения: `git.shts.su/denozord/auth-portal` +Публичный URL: `https://auth.shnt.top` +Compose: [`deploy/docker-compose.traefik.yml`](../deploy/docker-compose.traefik.yml) +Env-шаблон: [`deploy/env.traefik.example`](../deploy/env.traefik.example) -Документация: -- 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` +Документация Traefik: [Expose Docker](https://doc.traefik.io/traefik/expose/docker/basic/), [ACME DNS challenge](https://doc.traefik.io/traefik/https/acme/). + +``` +Internet → :80/:443 (Traefik) → auth-portal:8080 + ↑ + Cloudflare DNS TXT (ACME) +``` ## Предпосылки -1. Зона `shnt.top` (или ваш домен) в Cloudflare. -2. Docker на сервере; сеть для Traefik (часто `proxy`). -3. Доступ к registry: `docker login git.shts.su`. -4. Каталоги: +1. Зона домена в Cloudflare (например `shnt.top`). +2. Docker Engine + Compose plugin на VPS. +3. Свободные порты **80** и **443** на хосте (этот стек сам поднимает Traefik). +4. `docker login git.shts.su`. - ```bash - mkdir -p /opt/traefik /opt/auth-portal/data - ``` +> Если на сервере уже крутится другой Traefik на 80/443 — либо остановите его, либо смените `TRAEFIK_HTTP_PORT` / `TRAEFIK_HTTPS_PORT` (и проброс снаружи). Два Traefik на одних портах не запустятся. --- -## HTTPS: DNS challenge Cloudflare +## 1. Cloudflare: токен и DNS -HTTP-01 на порту 80 не нужен: Traefik создаёт TXT `_acme-challenge…` в Cloudflare и получает сертификат Let's Encrypt. Удобно, если порт 80 закрыт, домен за CF proxy, или нужен wildcard. +### API-токен -### 1. API-токен Cloudflare - -[Cloudflare Dashboard → My Profile → API Tokens → Create Token](https://dash.cloudflare.com/profile/api-tokens) - -Шаблон **Edit zone DNS** или Custom: +[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). +**Zone Resources:** Include → Specific zone → ваша зона. -Скопируйте токен один раз → это `CF_DNS_API_TOKEN`. -Не путать с Global API Key и не класть токен в репозиторий. +Токен → `CF_DNS_API_TOKEN` в `.env` стека. Не Global API Key, не в Git. -### 2. DNS-запись для портала - -В Cloudflare → DNS → Records: +### DNS-запись | Type | Name | Content | Proxy | |------|------|---------|-------| -| `A` (или `AAAA`) | `auth` | публичный IP сервера | **DNS only** (серое облако) | - -Для origin за Traefik на VPS обычно **DNS only**. Orange cloud (proxied) возможен, но тогда SSL/режимы CF настраиваются отдельно; ACME DNS-01 от этого не зависит. - -Проверка: +| `A` / `AAAA` | `auth` | IP VPS | **DNS only** (серое облако) | ```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}}' -``` +Traefik для сертификата создаёт TXT `_acme-challenge.auth…` через Cloudflare API (HTTP-01 на :80 не обязателен). --- -## Переменные окружения Auth Portal +## 2. Файлы на сервере -Файл `/opt/auth-portal/.env` (не коммитить): +```bash +mkdir -p /opt/auth-portal/data +cd /opt/auth-portal -```env -# Обязательно: общий секрет с VPS Tracker / CFDM (AUTH_JWT_SECRET) -JWT_SECRET=<длинная-случайная-строка> +# из репозитория или curl: +# deploy/docker-compose.traefik.yml → docker-compose.yml +# deploy/env.traefik.example → .env -ISSUER=https://auth.shnt.top -JWT_TTL_HOURS=1 -REFRESH_TTL_DAYS=14 +curl -fsSL -o docker-compose.yml \ + https://git.shts.su/denozord/auth-portal/raw/branch/main/deploy/docker-compose.traefik.yml +curl -fsSL -o .env \ + https://git.shts.su/denozord/auth-portal/raw/branch/main/deploy/env.traefik.example -# 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 +nano .env # заполнить секреты ``` +### Обязательные переменные в `.env` + | Переменная | Назначение | |------------|------------| -| `JWT_SECRET` | HS256 для access JWT; тот же секрет в приложениях | -| `ISSUER` | Должен совпадать с `AUTH_ISSUER` в приложениях | -| `RETURN_TO_ALLOWLIST` | Иначе SSO `return_to` отклоняется | -| `ADMIN_*` | Первый админ при пустом `/data/app.db` | -| `TRAEFIK_CERTRESOLVER` | Имя resolver'а Traefik (`letsencrypt`) | +| `CF_DNS_API_TOKEN` | Cloudflare token для ACME DNS-01 (env контейнера **Traefik**) | +| `LETSENCRYPT_EMAIL` | Email для Let's Encrypt | +| `JWT_SECRET` | HS256; тот же секрет в VPS Tracker / CFDM (`AUTH_JWT_SECRET`) | +| `ADMIN_PASSWORD` | Пароль bootstrap-админа (только при пустой БД) | +| `AUTH_DOMAIN` | Хост в Traefik rule, по умолчанию `auth.shnt.top` | +| `ISSUER` | `https://auth.shnt.top` — совпадает с `AUTH_ISSUER` приложений | +| `RETURN_TO_ALLOWLIST` | Origins SSO (например `.shnt.top`) | -В production cookie refresh ставится с флагом `Secure` — нужен HTTPS (Traefik). +Опционально: `AUTH_IMAGE_TAG`, `TRAEFIK_IMAGE_TAG`, `TRAEFIK_HTTP_PORT`, `TRAEFIK_HTTPS_PORT`. + +В production cookie refresh с флагом `Secure` — нужен HTTPS. --- -## Вариант A — Docker Compose - -Готовый файл в репозитории: [`deploy/docker-compose.traefik.yml`](../deploy/docker-compose.traefik.yml). - -На сервере: +## 3. Запуск (один compose) ```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 login git.shts.su docker compose pull docker compose up -d docker compose ps docker compose logs -f --tail=100 ``` -После старта Traefik запросит сертификат (DNS TXT в Cloudflare). Первая выдача может занять 30–90 с. +Сервисы: -Проверка: +| Service | Контейнер | Роль | +|---------|-----------|------| +| `traefik` | `auth-portal-traefik` | :80 → HTTPS, ACME, роутинг | +| `app` | `auth-portal` | приложение на внутренней сети `auth-portal`, порт 8080 | + +Первая выдача сертификата обычно 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 +echo | openssl s_client -connect auth.shnt.top:443 -servername auth.shnt.top 2>/dev/null \ + | openssl x509 -noout -issuer -dates -subject + +docker compose logs traefik 2>&1 | grep -iE 'acme|certificate|cloudflare|error' ``` -Обновление образа: +### Обновление / остановка ```bash cd /opt/auth-portal docker compose pull docker compose up -d -``` -Остановка: - -```bash +# остановка (тома и ./data сохраняются) docker compose down -# данные в ./data сохраняются (не используйте -v без бэкапа) +# НЕ делайте down -v без бэкапа — сотрёт ACME (auth_portal_traefik_letsencrypt) ``` --- -## Вариант B — Docker CLI +## Что внутри compose -Подставьте имя сети Traefik вместо `proxy`, если нужно. +- Сеть Docker **`auth-portal`** (внутренняя, создаётся стеком). +- Том **`auth_portal_traefik_letsencrypt`** → `/letsencrypt/acme.json`. +- Том хоста **`./data`** → SQLite портала. +- Labels на `app`: `Host(AUTH_DOMAIN)`, `entrypoints=websecure`, `certresolver=letsencrypt`, backend port `8080`. +- `CF_DNS_API_TOKEN` только у сервиса `traefik`. -```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`. +Полный файл: [`deploy/docker-compose.traefik.yml`](../deploy/docker-compose.traefik.yml). --- ## Связка с приложениями -После деплоя портала в VPS Tracker / CFDM: - ```env AUTH_REQUIRED=true AUTH_JWT_SECRET=<тот же JWT_SECRET> @@ -315,28 +163,23 @@ 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=…`). +См. [integrate-vps-tracker.md](integrate-vps-tracker.md), [integrate-cfdm.md](integrate-cfdm.md). +Logout SSO: `https://auth.shnt.top/logout`. --- -## Бэкап и откат +## Бэкап ```bash -# бэкап SQLite +# 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 \ +# ACME +docker run --rm -v auth_portal_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` без необходимости. +Откат образа: в `.env` `AUTH_IMAGE_TAG=vX.Y.Z` → `docker compose pull && docker compose up -d`. --- @@ -344,27 +187,21 @@ docker pull git.shts.su/denozord/auth-portal:vX.Y.Z | Симптом | Что проверить | |---------|----------------| -| 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: +| `Bind for 0.0.0.0:80/443 failed` | Другой Traefik/nginx занимает порты | +| ACME / нет HTTPS | `CF_DNS_API_TOKEN`, права Zone:DNS:Edit, логи `docker compose logs traefik` | +| `invalid credentials` | Не Global Key; зона в scope токена; пробелы в `.env` | +| Gateway Timeout / 404 | `docker compose ps`; labels; сеть `auth-portal` | +| Login OK, SSO в app падает | `JWT_SECRET` / `ISSUER` | +| `return_to` rejected | `RETURN_TO_ALLOWLIST` | +| Cookie не держится | HTTPS; `NODE_ENV=production` | ```bash -docker logs traefik 2>&1 | grep -iE 'acme|certificate|cloudflare|error' +docker compose logs traefik 2>&1 | grep -iE 'acme|certificate|cloudflare|error' +docker compose logs app --tail=50 ``` -Локальный 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 -``` +## Альтернатива: Docker CLI (без compose) + +Если нужен ручной запуск — создайте сеть и два контейнера с теми же env/labels, что в compose. Предпочтительный путь — **один `docker compose up -d`** выше.