# Deployment Guide ## Docker Deployment AI Weekly Synth is designed for Docker-only deployment. The `docker-compose.yml` at the project root orchestrates the application and its PostgreSQL database. ### Quick Start ```bash # 1. Clone the repository git clone cd ai_synth # 2. Create and configure .env cp .env.example .env # Edit .env and fill in all values (see Environment Variables below) # 3. Start the stack docker compose up -d # 4. Create the first admin user docker exec ai-synth ./ai-synth-backend create-admin admin@example.com ``` The application will be available at `http://localhost:8080` (or the port configured in `PORT`). ### Docker Compose Services The `docker-compose.yml` defines two services: **app** (AI Weekly Synth backend + frontend): - Multi-stage Docker image: Node.js builds the frontend, Rust builds the backend, then both are combined into a minimal Debian runtime - Runs as a non-root user (`appuser`) - Depends on `db` with a health check condition (waits for Postgres to be ready) - Health check: `curl -f http://localhost:8080/api/v1/health` every 30 seconds - Restart policy: `unless-stopped` **db** (PostgreSQL 17 Alpine): - Data persisted to a named Docker volume (`postgres_data`) - Exposed on `127.0.0.1:5432` (localhost only, not accessible from external networks) - Health check: `pg_isready` every 10 seconds - Shared memory: 128 MB - Restart policy: `unless-stopped` ### Dockerfile Details The `backend/Dockerfile` uses a three-stage build: 1. **frontend-builder** (Node.js 22 Alpine): Runs `npm ci` and `npm run build` to produce the static frontend in `/app/dist/` 2. **builder** (Rust 1.88 Bookworm): Compiles the Rust backend in release mode with `SQLX_OFFLINE=true` (no live database needed during build) 3. **runtime** (Debian Bookworm Slim): Installs only `ca-certificates`, `libssl3`, and `curl`. Copies the binary, migrations, and frontend static files. Runs as non-root. --- ## Environment Variables All environment variables are documented in `.env.example`. The `.env` file is loaded by Docker Compose. ### Required | Variable | Description | Example | |----------|-------------|---------| | `DATABASE_URL` | PostgreSQL connection string. In docker-compose, the hostname is `db`. | `postgres://ai_synth:secret@db:5432/ai_synth` | | `POSTGRES_PASSWORD` | Password for the PostgreSQL user. Used by both the `db` service and in `DATABASE_URL`. | `a-strong-random-password` | | `MASTER_ENCRYPTION_KEY` | 256-bit key for AES-256-GCM encryption of user API keys at rest. Must be exactly 64 hex characters. Generate with `openssl rand -hex 32`. **Back this up securely -- losing it means all stored API keys become unreadable.** | `ab12cd34...` (64 hex chars) | | `APP_URL` | Public URL where the app is accessible (no trailing slash). Used for magic link URLs, CORS origin, and cookie domain. | `https://synth.example.com` | | `RESEND_API_KEY` | API key for Resend (email service). Required for magic link emails and synthesis email export. Sign up at https://resend.com. | `re_xxxxx` | | `EMAIL_FROM` | Sender address for emails. Must be a verified domain in Resend. | `AI Weekly Synth ` | | `TURNSTILE_SECRET_KEY` | Server-side secret key for Cloudflare Turnstile captcha. Sign up at https://dash.cloudflare.com/turnstile. | `0x4AAAAAAA...` | | `TURNSTILE_SITE_KEY` | Client-side site key for Cloudflare Turnstile. | `0x4BBBBBB...` | ### Optional | Variable | Description | Default | |----------|-------------|---------| | `PORT` | Port for the backend HTTP server (inside the container). The docker-compose maps this to the host. | `8080` | | `RUST_LOG` | Logging level. Format: `level` or `level,crate=level`. | `info,ai_synth_backend=debug` | | `STATIC_DIR` | Path to the built frontend files. In Docker, this is `./static` (set by docker-compose). For local dev, use `../frontend/dist`. | `./static` (Docker) | | `SESSION_SECRET` | Secret for session cookie signing. At least 64 characters. If not set, a random value is generated at startup (sessions will not survive restarts). | Random | --- ## Database ### PostgreSQL The application uses PostgreSQL 17. The `docker-compose.yml` runs it as the `db` service with a named volume for data persistence. Key configuration: - User: `ai_synth` (configurable via `POSTGRES_PASSWORD`) - Database: `ai_synth` - Shared memory: 128 MB (for complex queries) - Health check via `pg_isready` ### Automatic Migrations Database migrations run automatically every time the application starts. The backend calls `sqlx::migrate!("./migrations")` in `main.rs` before starting the HTTP server. There are currently 30 migration files covering all schema changes from initial setup through themes, schedules, article history, and LLM call logging. No manual migration step is needed. The application will not start serving requests until migrations complete successfully. ### Tables The database contains the following tables: | Table | Purpose | |-------|---------| | `users` | User accounts (email, display name, role) | | `sessions` | Active sessions (hashed tokens, expiry) | | `magic_link_tokens` | Passwordless login tokens | | `user_settings` | Per-user configuration (provider, model, batch size, etc.) | | `sources` | User-defined news sources (URLs, titles, themes) | | `syntheses` | Generated synthesis results (sections as JSONB) | | `admin_providers` | Admin-curated LLM providers and models | | `admin_rate_limits` | Admin-configured rate limits per provider | | `user_api_keys` | Encrypted LLM API keys | | `audit_log` | Admin action audit trail | | `article_history` | Previously seen article URLs for dedup | | `llm_call_log` | LLM API call logs (prompts, responses, timing) | | `themes` | User-defined synthesis themes (topic, categories, settings) | | `theme_schedules` | Automated generation schedules per theme | --- ## Background Tasks The application starts two background tasks automatically on startup. No external cron or scheduler is needed. ### Session Cleanup (hourly) Every hour, a background task deletes expired sessions from the `sessions` table. This prevents unbounded growth of the sessions table. The task logs the number of deleted sessions. ### Scheduled Synthesis Generation (every 60 seconds) Every 60 seconds, a background task checks for due theme schedules (matching the current day of the week and time in UTC). For each due schedule, it: 1. Runs the synthesis generation pipeline for the associated theme 2. Sends the result via email to the configured recipients (up to 3) 3. Marks the schedule as run (updates `last_run_at`) to prevent re-execution on the same day This is a single-instance scheduler -- it does not use distributed locks. Do not run multiple instances of the application if scheduled generation is enabled (it would cause duplicate executions). --- ## Monitoring ### Health Check The `/api/v1/health` endpoint returns HTTP 200 when the application is running and can serve requests. It is used by: - Docker's built-in health check (configured in both `docker-compose.yml` and `Dockerfile`) - External monitoring tools ```bash curl -f http://localhost:8080/api/v1/health ``` ### Logs The application uses structured logging via the `tracing` crate. Log level is controlled by the `RUST_LOG` environment variable. Recommended production setting: ``` RUST_LOG=info,ai_synth_backend=debug ``` This provides: - `info` level for all crates (HTTP requests, startup/shutdown, background tasks) - `debug` level for the application code (detailed pipeline progress, LLM call timing) Logs go to stdout, which Docker captures and makes available via `docker logs ai-synth`. To view logs: ```bash docker logs ai-synth # all logs docker logs ai-synth --tail 100 # last 100 lines docker logs ai-synth -f # follow live ``` --- ## Backup ### Database The PostgreSQL data volume (`postgres_data`) is the only stateful component. Back it up regularly: ```bash # Dump the database docker exec ai-synth-db pg_dump -U ai_synth ai_synth > backup_$(date +%Y%m%d).sql # Restore from a dump cat backup_20260327.sql | docker exec -i ai-synth-db psql -U ai_synth ai_synth ``` ### No File Storage The application does not store files on disk. All data (syntheses, settings, API keys, article history) lives in PostgreSQL. The frontend is served from static files baked into the Docker image. ### Encryption Key The `MASTER_ENCRYPTION_KEY` is critical. If lost, all user API keys stored in the database become permanently unreadable. Store it securely (e.g., in a secrets manager) and include it in your disaster recovery plan. --- ## Updating To update to a new version: ```bash # 1. Pull the latest code git pull # 2. Rebuild the Docker image and restart docker compose up -d --build ``` This will: 1. Rebuild the Docker image (frontend build + Rust compilation) 2. Restart the `app` container with the new image 3. Automatically run any new migrations on startup 4. The `db` container is unaffected (data persists in the named volume) The restart causes a brief downtime (typically 10-30 seconds for the health check to pass). For zero-downtime deployments, consider running behind a reverse proxy with health-check-based routing. --- ## Security Checklist Before deploying to production, verify: - [ ] **`MASTER_ENCRYPTION_KEY`** is set to a random 64 hex character value (not the example value). Generated with `openssl rand -hex 32`. Stored securely and backed up. - [ ] **`POSTGRES_PASSWORD`** is set to a strong random password. - [ ] **HTTPS** is configured. Set `APP_URL` to an `https://` URL. The application sets `Secure` on cookies when `APP_URL` starts with `https`. Use a reverse proxy (nginx, Caddy, Traefik) to terminate TLS. - [ ] **Turnstile** keys are configured. Without them, the registration and login forms will not work (captcha is required). - [ ] **Resend** API key is configured with a verified sending domain. - [ ] **`SKIP_SSRF_CHECK`** is NOT set. This env var disables SSRF protection and should only be used in test environments. - [ ] **Postgres** is not exposed to the internet. The docker-compose binds it to `127.0.0.1:5432` by default. - [ ] **Docker socket** is not exposed. The app does not need Docker access. - [ ] **Firewall** allows inbound traffic only on the app port (8080 or whichever port is mapped). - [ ] **Reverse proxy** is configured to forward `X-Forwarded-For` and `X-Forwarded-Proto` headers if the app is behind a proxy. ### Security Features (Built-in) See `architecture.md` Section 6 for detailed security architecture.