# Trial deploy — 6 active lessons (no public launch checklist) This document prepares a **trial** stack using the existing Docker Compose layout. It does **not** deploy to a real server from this repo. ## Architecture (default) ``` Browser → [TLS terminator — your choice] → frontend nginx :80 ├─ / → Vue SPA (try_files → index.html) ├─ /api/* → proxy → backend Nest :3000 └─ /health → proxy → backend /health Backend → SQLite file on named volume (single instance) ``` | Component | Image / build | User | |-----------|---------------|------| | Frontend | `frontend/Dockerfile` → nginx:alpine | nginx (non-root worker) | | Backend | `backend-nest/Dockerfile` → node:22.18.0-slim | `node` | | Data | Docker volume `child-gold-land-sqlite` → `/app/data/app.db` | writable by `node` | **Not supported without proof:** SQLite on NFS/network shares, horizontal multi-instance backend with this rate limiter. ## Placeholders (set on server — never commit) Copy `frontend/.env.docker.example` → `frontend/.env.docker`: | Variable | Example placeholder | |----------|---------------------| | `JWT_SECRET` | `` | | `CORS_ORIGIN` | `https://` or `http://:8080` | | `VITE_API_URL` | `/api` (keep for same-origin nginx proxy) | | `FRONTEND_PORT` | `8080` | ### Proxy trust boundary (compose default) | Variable | Compose default | Meaning | |----------|-----------------|--------| | `TRUST_PROXY` | `true` | Use `X-Real-IP` / `X-Forwarded-For` **only** when TCP peer is in `TRUST_PROXY_CIDRS` | | `TRUST_PROXY_CIDRS` | Explicit list in compose (trial) | **Production:** set the CIDR of the nginx→backend hop (e.g. `docker network inspect child-gold-land`); backend **refuses** `TRUST_PROXY=true` without `TRUST_PROXY_CIDRS` when `NODE_ENV=production` | - **Fastify `trustProxy` stays off** — client IP for rate limits comes from `getClientIp()` (trusted peer + single-hop headers). - nginx sets `X-Forwarded-For $remote_addr` (replaces forged client chains). - **Backend is not published on the host** — only frontend port is exposed; direct access to Nest bypasses nginx and should not be exposed in trial deploy. **Rate limiting:** in-memory, **per client IP**, **single backend instance**. Many users behind one NAT share one bucket. For stricter limits use an edge proxy or account-based throttling (out of scope). **Do not** expose backend port `3000` on the host in production trial. ## Build and start (local trial) From sibling clones (`frontend/`, `backend-nest/`): ```powershell cd frontend copy .env.docker.example .env.docker # Edit .env.docker — set JWT_SECRET and CORS_ORIGIN docker compose config # validate compose file docker compose build docker compose up -d ``` Checks: ```powershell curl http://localhost:8080/health curl http://localhost:8080/health/ready curl http://localhost:8080/api/... # via app UI login ``` Vue routes: refresh on `/map`, `/market`, etc. must return `index.html` (configured in `nginx.conf`). ## HTTPS - Terminate TLS at **your** reverse proxy (Caddy, Traefik, nginx on host) or cloud LB. - Redirect HTTP→HTTPS at the terminator. - Set `CORS_ORIGIN` to the **exact** browser origin (`https://host`, no trailing slash). - Do not generate real certificates in this repo. ## Persistent data - **Path in container:** `/app/data/app.db` (+ `-wal`/`-shm` when WAL mode active) - **Volume:** `child-gold-land-sqlite` (compose) - **Host permissions:** ensure volume UID/GID allows `node` (uid 1000 in image) to read/write; if bind-mounting a host dir: `chown 1000:1000 ` Startup **never** replaces an existing DB with seed data. Migrations run forward-only on boot; failure aborts startup. ## Backup and restore On backend repo (host or one-off container with volume mounted): ```powershell cd backend-nest $env:DB_PATH = "/path/to/app.db" # or container path $env:BACKUP_DIR = "/path/to/backups" $env:BACKUP_RETENTION_DAYS = "14" npm run backup:sqlite npm run restore:sqlite -- backups/app-....db /tmp/restored.db # Stop backend first. Restore to a sibling path (not live DB_PATH), then swap files — see backend-nest/docs/restore-production-db.md. # RESTORE_ACK_STOPPED=YES = operator ack only (not automatic stop detection). ``` Uses SQLite **backup API** (WAL-safe), then `integrity_check`. ## Health vs readiness | Endpoint | Meaning | |----------|---------| | `GET /health` | Liveness — process up | | `GET /health/ready` | Readiness — DB migrated | Compose `depends_on: service_healthy` uses **ready**. ## Rollback | Action | Effect | |--------|--------| | `docker compose pull` + run previous image tags | Rolls back **code** only | | DB | **Not** rolled back with image — restore from backup if needed | | Forward migrations | Newer code may require newer schema; rolling back code without DB restore can break — test rollback pairs in staging | ## Safe shutdown ```powershell docker compose down # Data remains in volume child-gold-land-sqlite ``` ## CI backend pin (E2E) No silent `main` fallback: set Gitea variable **`E2E_BACKEND_REF`** to backend **commit SHA**. See `e2e/backend.contract.json` and frontend README. Secrets (names only): `E2E_BACKEND_CHECKOUT_TOKEN`. ## Still required for real deploy - [ ] Registered Gitea **act_runner** (CI not verified here) - [ ] Trial host OS, domain, TLS certificates - [ ] Strong `JWT_SECRET` on server - [ ] Off-site backup storage - [ ] Monitoring / log aggregation (no stack traces to clients — API returns generic `خطای سرور`)