10 KiB
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
# 1. Clone the repository
git clone <repo-url>
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
dbwith a health check condition (waits for Postgres to be ready) - Health check:
curl -f http://localhost:8080/api/v1/healthevery 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_isreadyevery 10 seconds - Shared memory: 128 MB
- Restart policy:
unless-stopped
Dockerfile Details
The backend/Dockerfile uses a three-stage build:
- frontend-builder (Node.js 22 Alpine): Runs
npm ciandnpm run buildto produce the static frontend in/app/dist/ - builder (Rust 1.88 Bookworm): Compiles the Rust backend in release mode with
SQLX_OFFLINE=true(no live database needed during build) - runtime (Debian Bookworm Slim): Installs only
ca-certificates,libssl3, andcurl. 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 <noreply@synth.example.com> |
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 viaPOSTGRES_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:
- Runs the synthesis generation pipeline for the associated theme
- Sends the result via email to the configured recipients (up to 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.ymlandDockerfile) - External monitoring tools
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:
infolevel for all crates (HTTP requests, startup/shutdown, background tasks)debuglevel 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:
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:
# 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:
# 1. Pull the latest code
git pull
# 2. Rebuild the Docker image and restart
docker compose up -d --build
This will:
- Rebuild the Docker image (frontend build + Rust compilation)
- Restart the
appcontainer with the new image - Automatically run any new migrations on startup
- The
dbcontainer 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_KEYis set to a random 64 hex character value (not the example value). Generated withopenssl rand -hex 32. Stored securely and backed up.POSTGRES_PASSWORDis set to a strong random password.- HTTPS is configured. Set
APP_URLto anhttps://URL. The application setsSecureon cookies whenAPP_URLstarts withhttps. 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_CHECKis 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:5432by 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-ForandX-Forwarded-Protoheaders if the app is behind a proxy.
Security Features (Built-in)
See architecture.md Section 6 for detailed security architecture.