# Docker Setup for GridPilot This document describes the Docker setup for local development and production deployment of GridPilot. ## Quick Start ### Development Start all services with hot-reloading: ```bash npm run docker:dev:build ``` This will: - Start PostgreSQL database on port 5432 - Start API on port 3001 (container port 3000, debugger 9229) - Start Website on port 3000 - Enable hot-reloading for both apps Access: - Website: http://localhost:3000 - API: http://localhost:3001 - Database: localhost:5432 ### Production Start all services in production mode: ```bash npm run docker:prod:build ``` This will: - Build optimized Docker images - Start PostgreSQL, Redis, API, Website, and Nginx - Enable health checks, auto-restart, and resource limits - Configure caching and performance optimizations Access: - Nginx (Website + API): http://localhost:80 ## Available Commands ### Development - `npm run docker:dev` - Start dev environment (alias of `docker:dev:up`) - `npm run docker:dev:up` - Start dev environment - `npm run docker:dev:postgres` - Start dev environment with `GRIDPILOT_API_PERSISTENCE=postgres` - `npm run docker:dev:inmemory` - Start dev environment with `GRIDPILOT_API_PERSISTENCE=inmemory` - `npm run docker:dev:build` - Rebuild and start - `npm run docker:dev:restart` - Restart services - `npm run docker:dev:ps` - Show service status - `npm run docker:dev:down` - Stop services - `npm run docker:dev:logs` - View logs - `npm run docker:dev:clean` - Stop and remove volumes ### Production - `npm run docker:prod` - Start prod environment - `npm run docker:prod:build` - Rebuild and start - `npm run docker:prod:down` - Stop services - `npm run docker:prod:logs` - View logs - `npm run docker:prod:clean` - Stop and remove volumes ### Testing (Docker) The goal of Docker-backed tests is to catch *wiring* issues between Website ↔ API (wrong hostnames/ports/env vars, missing CORS for credentialed requests, etc.) in a deterministic environment. - `npm run test:docker:website` - Start a dedicated test stack (ports `3100/3101`) and run Playwright smoke tests against it. - Uses [`docker-compose.test.yml`](docker-compose.test.yml:1). - Runs the Website in Docker and talks to an **API mock** container. - Validates that pages render and that core API fetches succeed (hostname + CORS + routing). Supporting scripts: - `npm run docker:test:deps` - Install monorepo deps inside a reusable Docker volume (one-shot). - `npm run docker:test:up` - Bring up the test stack. - `npm run docker:test:wait` - Wait for `http://localhost:3100` and `http://localhost:3101/health` to be ready. - `npm run docker:test:down` - Tear the test stack down (including volumes). ## Environment Variables ### "Mock vs Real" (Website & API) There is **no** `AUTOMATION_MODE` equivalent for the Website/API runtime. - **Website "mock vs real"** is controlled purely by *which API base URL you point it at* via [`getWebsiteApiBaseUrl()`](apps/website/lib/config/apiBaseUrl.ts:6): - Browser calls use `NEXT_PUBLIC_API_BASE_URL` - Server/Next.js calls use `API_BASE_URL ?? NEXT_PUBLIC_API_BASE_URL` - **API "mock vs real"** is controlled by API runtime env: - Persistence: `GRIDPILOT_API_PERSISTENCE=postgres|inmemory` in [`AppModule`](apps/api/src/app.module.ts:25) - Optional bootstrapping: `GRIDPILOT_API_BOOTSTRAP=0|1` in [`AppModule`](apps/api/src/app.module.ts:35) Practical presets: - **Website + real API (Docker dev)**: `npm run docker:dev:build` (Website `3000`, API `3001`, Postgres required). - Website browser → API: `NEXT_PUBLIC_API_BASE_URL=http://localhost:3001` - Website container → API container: `API_BASE_URL=http://api:3000` - **Website + mock API (Docker smoke)**: `npm run test:docker:website` (Website `3100`, API mock `3101`). - API mock is defined inline in [`docker-compose.test.yml`](docker-compose.test.yml:24) - Website browser → API mock: `NEXT_PUBLIC_API_BASE_URL=http://localhost:3101` - Website container → API mock container: `API_BASE_URL=http://api:3000` ### Website ↔ API Connection The website talks to the API via `fetch()` in [`BaseApiClient`](apps/website/lib/api/base/BaseApiClient.ts:11), and it always includes cookies (`credentials: 'include'`). That means: - The **browser** must be pointed at a host-accessible API URL via `NEXT_PUBLIC_API_BASE_URL` - The **server** (Next.js / Node) must be pointed at a container-network API URL via `API_BASE_URL` (when running in Docker) The single source of truth for "what base URL should I use?" is [`getWebsiteApiBaseUrl()`](apps/website/lib/config/apiBaseUrl.ts:6): - Browser: reads `NEXT_PUBLIC_API_BASE_URL` - Server: reads `API_BASE_URL ?? NEXT_PUBLIC_API_BASE_URL` - In Docker/CI/test: throws if missing (no silent localhost fallback) #### Dev Docker defaults (docker-compose.dev.yml) - Website: `http://localhost:3000` - API: `http://localhost:3001` (maps to container `api:3000`) - `NEXT_PUBLIC_API_BASE_URL=http://localhost:3001` (browser → host port) - `API_BASE_URL=http://api:3000` (website container → api container) #### Test Docker defaults (docker-compose.test.yml) This stack is intended for deterministic smoke tests and uses different host ports to avoid colliding with `docker:dev`: - Website: `http://localhost:3100` - API mock: `http://localhost:3101` (maps to container `api:3000`) - `NEXT_PUBLIC_API_BASE_URL=http://localhost:3101` (browser → host port) - `API_BASE_URL=http://api:3000` (website container → api container) Important: the test stack's API is a mock server defined inline in [`docker-compose.test.yml`](docker-compose.test.yml:24). It exists to validate Website ↔ API wiring, not domain correctness. #### Troubleshooting - If `docker:dev` is running, use `npm run docker:dev:down` before `npm run test:docker:website` to avoid port conflicts. - If Docker volumes get stuck, run `npm run docker:test:down` (it uses `--remove-orphans` + `rm -f`). ### API "Real vs In-Memory" Mode The API can now be run either: - **postgres**: loads [`DatabaseModule`](apps/api/src/domain/database/DatabaseModule.ts:1) (requires Postgres) - **inmemory**: does not load `DatabaseModule` (no Postgres required) Control it with: - `GRIDPILOT_API_PERSISTENCE=postgres|inmemory` (defaults to `postgres` if `DATABASE_URL` is set, otherwise `inmemory`) - Optional: `GRIDPILOT_API_BOOTSTRAP=0` to skip [`BootstrapModule`](apps/api/src/domain/bootstrap/BootstrapModule.ts:1) ### Development (.env.development) Copy and customize as needed. Default values work out of the box. ### Production (.env.production) **IMPORTANT**: Update these before deploying: - Database credentials (`POSTGRES_PASSWORD`, `DATABASE_URL`) - Website/API URLs (`NEXT_PUBLIC_API_BASE_URL`, `NEXT_PUBLIC_SITE_URL`) - Vercel KV credentials (`KV_REST_API_URL`, `KV_REST_API_TOKEN`) (required for production email signups/rate limit) ## Architecture ### Development Setup - Hot-reloading enabled via volume mounts - Source code changes reflect immediately - Database persisted in named volume - Debug port exposed for API (9229) ### Production Setup - Multi-stage builds for optimized images - Only production dependencies included - Nginx reverse proxy for both services - Health checks for all services - Auto-restart on failure ## Docker Services ### API (NestJS) - Dev: `apps/api/Dockerfile.dev` - Prod: `apps/api/Dockerfile.prod` - Port: 3000 - Debug: 9229 (dev only) ### Website (Next.js) - Dev: `apps/website/Dockerfile.dev` - Prod: `apps/website/Dockerfile.prod` - Port: 3001 (dev), 3000 (prod) ### Database (PostgreSQL) - Image: postgres:15-alpine - Port: 5432 (internal) - Data: Persisted in Docker volume - Optimized with performance tuning parameters ### Redis (Production only) - Image: redis:7-alpine - Port: 6379 (internal) - Configured with: - LRU eviction policy - 512MB max memory - AOF persistence - Password protection ### Nginx (Production only) - Reverse proxy for website + API - Features: - Rate limiting (API: 10r/s, General: 30r/s) - Security headers (XSS, CSP, Frame-Options) - Gzip compression - Static asset caching - Connection pooling - Request buffering - Port: 80, 443 ## Troubleshooting ### Services won't start ```bash # Clean everything and rebuild npm run docker:dev:clean npm run docker:dev:build ``` ### Hot-reloading not working Check that volume mounts are correct in docker-compose.dev.yml ### Database connection issues Ensure DATABASE_URL in .env matches the database service configuration ### Check logs ```bash # All services npm run docker:dev:logs # Specific service docker-compose -f docker-compose.dev.yml logs -f api docker-compose -f docker-compose.dev.yml logs -f website docker-compose -f docker-compose.dev.yml logs -f db ``` ### Database Migration for Media References If you have existing seeded data with old URL formats (e.g., `/api/avatar/{id}`, `/api/media/teams/{id}/logo`), you need to migrate to the new `MediaReference` format. #### Option 1: Migration Script (Preserve Data) Run the migration script to convert old URLs to proper `MediaReference` objects: ```bash # Test mode (dry run - shows what would change) npm run migrate:media:test # Execute migration (applies changes) npm run migrate:media:exec ``` The script handles: - **Driver avatars**: `/api/avatar/{id}` → `system-default` (deterministic variant) - **Team logos**: `/api/media/teams/{id}/logo` → `generated` - **League logos**: `/api/media/leagues/{id}/logo` → `generated` - **Unknown formats** → `none` #### Option 2: Wipe and Reseed (Clean Slate) For development environments, you can wipe all data and start fresh: ```bash # Stop services and remove volumes npm run docker:dev:clean # Rebuild and start fresh npm run docker:dev:build ``` This will: - Delete all existing data - Run fresh seed with correct `MediaReference` format - No migration needed #### When to Use Each Option **Use Migration Script** when: - You have production data you want to preserve - You want to understand what changes will be made - You need a controlled, reversible process **Use Wipe and Reseed** when: - You're in development/testing - You don't care about existing data - You want the fastest path to a clean state ## Tips 1. **First time setup**: Use `docker:dev:build` to ensure images are built 2. **Clean slate**: Use `docker:dev:clean` to remove all data and start fresh 3. **Production testing**: Test prod setup locally before deploying 4. **Database access**: Use any PostgreSQL client with credentials from .env file 5. **Debugging**: Attach debugger to port 9229 for API debugging ## Production Deployment Before deploying to production: 1. Update `.env.production` with real credentials 2. Configure SSL certificates in `nginx/ssl/` 3. Update Nginx configuration for HTTPS 4. Set proper domain names in environment variables 5. Consider using Docker secrets for sensitive data ## File Structure ``` . ├── docker-compose.dev.yml # Development orchestration ├── docker-compose.prod.yml # Production orchestration ├── .env.development # Dev environment variables ├── .env.production # Prod environment variables ├── apps/ │ ├── api/ │ │ ├── Dockerfile.dev # API dev image │ │ ├── Dockerfile.prod # API prod image │ │ └── .dockerignore │ └── website/ │ ├── Dockerfile.dev # Website dev image │ ├── Dockerfile.prod # Website prod image │ └── .dockerignore └── nginx/ └── nginx.conf # Nginx configuration