Files
gridpilot.gg/README.docker.md
2025-12-26 20:54:20 +01:00

261 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
- `npm run docker:dev:build` - Rebuild and start
- `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 stacks 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
```
## 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