docs: update deployment instructions for Auth Portal with Traefik in a single Docker Compose stack
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 19:33:22 +07:00
parent 638df5d679
commit b5a50ac3b3
4 changed files with 178 additions and 274 deletions
+2 -2
View File
@@ -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)
## Структура
+57 -14
View File
@@ -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
+24
View File
@@ -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
+95 -258
View File
@@ -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`** выше.