From 496b0ff62878e19e5970bd2a8d95186604bfe6d5 Mon Sep 17 00:00:00 2001 From: "yves.gugger" Date: Fri, 12 Dec 2025 12:22:29 +0100 Subject: [PATCH] docs: add server deployment guide and compose env wiring --- DEPLOY_docker_compose.env.example | 46 ++++++++ DEPLOY_frontend.env.example | 2 +- README.md | 4 + SERVER_DEPLOYMENT.md | 170 ++++++++++++++++++++++++++++++ backend/app/main.py | 17 +-- docker-compose.yml | 53 ++++++++-- frontend/src/lib/api.ts | 7 +- 7 files changed, 280 insertions(+), 19 deletions(-) create mode 100644 DEPLOY_docker_compose.env.example create mode 100644 SERVER_DEPLOYMENT.md diff --git a/DEPLOY_docker_compose.env.example b/DEPLOY_docker_compose.env.example new file mode 100644 index 0000000..f0c18b0 --- /dev/null +++ b/DEPLOY_docker_compose.env.example @@ -0,0 +1,46 @@ +# Docker Compose environment (NO SECRETS) +# +# Copy to `.env` (it is gitignored): +# cp DEPLOY_docker_compose.env.example .env +# +# Then set real values (password manager / vault). + +# Core (required) +DB_PASSWORD=change-me +SECRET_KEY=GENERATE_A_LONG_RANDOM_SECRET +ENVIRONMENT=production +SITE_URL=https://your-domain.com + +# CORS (only needed if frontend and backend are different origins) +ALLOWED_ORIGINS=https://your-domain.com,https://www.your-domain.com + +# Cookies (optional) +COOKIE_SECURE=true +# COOKIE_DOMAIN=.your-domain.com + +# Email (optional but recommended for alerts) +# SMTP_HOST=smtp.example.com +# SMTP_PORT=587 +# SMTP_USER= +# SMTP_PASSWORD= +# SMTP_FROM_EMAIL= +# SMTP_FROM_NAME=pounce +# SMTP_USE_TLS=true +# SMTP_USE_SSL=false +# CONTACT_EMAIL= + +# OAuth (optional) +# GOOGLE_CLIENT_ID= +# GOOGLE_CLIENT_SECRET= +# GOOGLE_REDIRECT_URI=https://your-domain.com/api/v1/oauth/google/callback +# GITHUB_CLIENT_ID= +# GITHUB_CLIENT_SECRET= +# GITHUB_REDIRECT_URI=https://your-domain.com/api/v1/oauth/github/callback + +# Stripe (optional) +# STRIPE_SECRET_KEY= +# STRIPE_WEBHOOK_SECRET= +# STRIPE_PRICE_TRADER= +# STRIPE_PRICE_TYCOON= + + diff --git a/DEPLOY_frontend.env.example b/DEPLOY_frontend.env.example index c21fa79..183f307 100644 --- a/DEPLOY_frontend.env.example +++ b/DEPLOY_frontend.env.example @@ -3,5 +3,5 @@ # Copy to a *local-only* file and keep it OUT of git: # cp DEPLOY_frontend.env.example DEPLOY_frontend.env # -NEXT_PUBLIC_API_URL=https://your-domain.com/api +NEXT_PUBLIC_API_URL=https://your-domain.com/api/v1 diff --git a/README.md b/README.md index cb0d955..246aea2 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ For the new cookie-auth to “just work”, the recommended setup is: - **Serve the frontend on your main domain** - **Route `/api/v1/*` to the backend via reverse proxy** (nginx/caddy/Next rewrite) +## Server deployment (recommended) + +See `SERVER_DEPLOYMENT.md`. + ### Env files (important) - **Never commit** any of these: diff --git a/SERVER_DEPLOYMENT.md b/SERVER_DEPLOYMENT.md new file mode 100644 index 0000000..42a80b0 --- /dev/null +++ b/SERVER_DEPLOYMENT.md @@ -0,0 +1,170 @@ +# Server Deployment (Docker Compose) + +## Ziel + +Pounce auf einem Server starten mit: + +- **Frontend** (Next.js) +- **Backend API** (FastAPI) +- **Postgres** +- **Redis** (Rate-Limit Storage + Job Queue) +- **Scheduler** (APScheduler) – **separater Prozess** +- **Worker** (ARQ) – **separater Prozess** + +Damit laufen Jobs nicht mehrfach bei mehreren API-Workern und die UI bleibt schnell. + +--- + +## Voraussetzungen + +- Linux Server (z.B. Ubuntu 22.04+) +- Docker + Docker Compose Plugin +- Domain + HTTPS Reverse Proxy (empfohlen), damit Cookie-Auth zuverlässig funktioniert + +--- + +## 1) Repo auf den Server holen + +```bash +cd /opt +git clone pounce +cd pounce +``` + +--- + +## 2) Server-Environment anlegen + +In `/opt/pounce`: + +```bash +cp DEPLOY_docker_compose.env.example .env +``` + +Dann `.env` öffnen und mindestens setzen: + +- **DB_PASSWORD** +- **SECRET_KEY** +- **SITE_URL** (z.B. `https://pounce.example.com`) +- **ALLOWED_ORIGINS** (z.B. `https://pounce.example.com`) + +Optional (aber empfohlen): + +- **SMTP_\*** (für Alerts/Emails) +- **COOKIE_DOMAIN** (wenn du Cookies über Subdomains teilen willst) + +--- + +## 3) Starten + +```bash +docker compose up -d --build +``` + +Services: + +- `frontend` (Port 3000) +- `backend` (Port 8000) +- `scheduler` (kein Port) +- `worker` (kein Port) +- `db` (kein Port) +- `redis` (kein Port) + + +--- + +## 4) Initial Setup (1× nach erstem Start) + +### DB Tabellen + Baseline Seed + +```bash +docker compose exec backend python scripts/init_db.py +``` + +### TLD Price Seed (886+) + +```bash +docker compose exec backend python scripts/seed_tld_prices.py +``` + +--- + +## 5) Reverse Proxy (empfohlen) + +### Warum? + +Das Frontend ruft im Browser standardmässig `https:///api/v1/...` auf (same-origin). +Darum solltest du: + +- **HTTPS** terminieren +- `/api/v1/*` an das Backend routen +- `/` an das Frontend routen + +### Beispiel: Caddy (sehr simpel) + +```caddy +pounce.example.com { + encode zstd gzip + + # API + handle_path /api/v1/* { + reverse_proxy 127.0.0.1:8000 + } + + # Frontend + reverse_proxy 127.0.0.1:3000 + + # optional: metrics nur intern + @metrics path /metrics + handle @metrics { + respond 403 + } +} +``` + +Wichtig: + +- Setze `SITE_URL=https://pounce.example.com` +- Setze `COOKIE_SECURE=true` (oder via `ENVIRONMENT=production`) + +--- + +## 6) Checks (nach Deploy) + +```bash +curl -f http://127.0.0.1:8000/health +curl -f http://127.0.0.1:8000/metrics +``` + +Logs: + +```bash +docker compose logs -f backend +docker compose logs -f scheduler +docker compose logs -f worker +``` + +--- + +## 7) Updates + +```bash +cd /opt/pounce +git pull +docker compose up -d --build +``` + +--- + +## Troubleshooting (häufig) + +- **Cookies/Login klappt nicht**: + - Prüfe `SITE_URL` und HTTPS (Secure Cookies) + - Prüfe `ALLOWED_ORIGINS` (falls Frontend/Backend nicht same-origin sind) +- **Scheduler läuft doppelt**: + - Stelle sicher, dass nur **ein** `scheduler` Service läuft (keine zweite Instanz) +- **Emails werden nicht gesendet**: + - `docker compose exec scheduler env | grep SMTP_` + - SMTP Vars müssen im Container vorhanden sein (kommen aus `.env`) + + diff --git a/backend/app/main.py b/backend/app/main.py index 5e2049d..f203ecd 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -118,14 +118,15 @@ async def rate_limit_handler(request: Request, exc: RateLimitExceeded): }, ) -# Get allowed origins from environment -ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "").split(",") -if not ALLOWED_ORIGINS or ALLOWED_ORIGINS == [""]: - ALLOWED_ORIGINS = [ - "http://localhost:3000", - "http://127.0.0.1:3000", - "http://10.42.0.73:3000", - ] +# Get allowed origins (env overrides settings) +origins_raw = ( + os.getenv("ALLOWED_ORIGINS", "").strip() + or os.getenv("CORS_ORIGINS", "").strip() + or (settings.cors_origins or "").strip() +) +ALLOWED_ORIGINS = [o.strip() for o in origins_raw.split(",") if o.strip()] +if not ALLOWED_ORIGINS: + ALLOWED_ORIGINS = ["http://localhost:3000", "http://127.0.0.1:3000"] # Add production origins SITE_URL = os.getenv("SITE_URL", "") diff --git a/docker-compose.yml b/docker-compose.yml index a57c78b..28651ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,45 @@ version: '3.8' +x-backend-env: &backend-env + DATABASE_URL: postgresql+asyncpg://pounce:${DB_PASSWORD:-changeme}@db:5432/pounce + SECRET_KEY: ${SECRET_KEY:-change-this-in-production} + ENVIRONMENT: ${ENVIRONMENT:-production} + SITE_URL: ${SITE_URL:-} + ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-} + COOKIE_DOMAIN: ${COOKIE_DOMAIN:-} + COOKIE_SECURE: ${COOKIE_SECURE:-true} + # Optional: SMTP (email alerts) + SMTP_HOST: ${SMTP_HOST:-} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-} + SMTP_FROM_NAME: ${SMTP_FROM_NAME:-pounce} + SMTP_USE_TLS: ${SMTP_USE_TLS:-true} + SMTP_USE_SSL: ${SMTP_USE_SSL:-false} + CONTACT_EMAIL: ${CONTACT_EMAIL:-} + # Optional: OAuth + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} + GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-} + GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-} + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-} + GITHUB_REDIRECT_URI: ${GITHUB_REDIRECT_URI:-} + # Optional: Stripe + STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-} + STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-} + STRIPE_PRICE_TRADER: ${STRIPE_PRICE_TRADER:-} + STRIPE_PRICE_TYCOON: ${STRIPE_PRICE_TYCOON:-} + # Optional integrations + DROPCATCH_CLIENT_ID: ${DROPCATCH_CLIENT_ID:-} + DROPCATCH_CLIENT_SECRET: ${DROPCATCH_CLIENT_SECRET:-} + DROPCATCH_API_BASE: ${DROPCATCH_API_BASE:-https://api.dropcatch.com} + SEDO_PARTNER_ID: ${SEDO_PARTNER_ID:-} + SEDO_SIGN_KEY: ${SEDO_SIGN_KEY:-} + SEDO_API_BASE: ${SEDO_API_BASE:-https://api.sedo.com/api/v1/} + MOZ_ACCESS_ID: ${MOZ_ACCESS_ID:-} + MOZ_SECRET_KEY: ${MOZ_SECRET_KEY:-} + services: # PostgreSQL Database db: @@ -42,9 +82,7 @@ services: ports: - "8000:8000" environment: - DATABASE_URL: postgresql+asyncpg://pounce:${DB_PASSWORD:-changeme}@db:5432/pounce - SECRET_KEY: ${SECRET_KEY:-change-this-in-production} - CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000} + <<: *backend-env ENABLE_SCHEDULER: "false" ENABLE_JOB_QUEUE: "true" REDIS_URL: redis://redis:6379/0 @@ -69,8 +107,7 @@ services: container_name: pounce-scheduler restart: unless-stopped environment: - DATABASE_URL: postgresql+asyncpg://pounce:${DB_PASSWORD:-changeme}@db:5432/pounce - SECRET_KEY: ${SECRET_KEY:-change-this-in-production} + <<: *backend-env ENABLE_SCHEDULER: "true" depends_on: db: @@ -85,8 +122,7 @@ services: container_name: pounce-worker restart: unless-stopped environment: - DATABASE_URL: postgresql+asyncpg://pounce:${DB_PASSWORD:-changeme}@db:5432/pounce - SECRET_KEY: ${SECRET_KEY:-change-this-in-production} + <<: *backend-env ENABLE_SCHEDULER: "false" ENABLE_JOB_QUEUE: "true" REDIS_URL: redis://redis:6379/0 @@ -107,7 +143,8 @@ services: ports: - "3000:3000" environment: - NEXT_PUBLIC_API_URL: ${API_URL:-http://localhost:8000} + # Internal backend URL for server-side requests + BACKEND_URL: http://backend:8000 depends_on: - backend diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index c9cb379..3d65c06 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -10,8 +10,11 @@ const getApiBase = (): string => { // Server-side rendering: use environment variable or localhost if (typeof window === 'undefined') { - const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1' - return baseUrl.replace(/\/$/, '') + // Prefer internal backend URL (server-only) when running in Docker/SSR + const backendUrl = process.env.BACKEND_URL?.replace(/\/$/, '') + const configured = (backendUrl ? `${backendUrl}/api/v1` : process.env.NEXT_PUBLIC_API_URL) || 'http://localhost:8000/api/v1' + const normalized = configured.replace(/\/$/, '') + return normalized.endsWith('/api/v1') ? normalized : `${normalized}/api/v1` } const { protocol, hostname, port } = window.location