Deployment
Docker Compose
The project uses separate Compose files for development and production.
Development (docker-compose.yml) — infrastructure only
Apps run on the host. PostgreSQL, Redis, MinIO and other services run in Docker.
# docker-compose.yml
services:
postgres:
image: postgres:16-alpine
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
mailpit:
image: axllent/mailpit:latest
ports:
- "1025:1025"
- "8025:8025"
minio:
image: minio/minio:latest
ports:
- "9000:9000"
- "9001:9001"
livekit:
image: livekit/livekit-server:latest
network_mode: hostProduction (docker-compose.prod.yml) — apps only
Infrastructure uses external managed services (RDS, ElastiCache, etc.).
# docker-compose.prod.yml
services:
migration:
image: ${REGISTRY_HOST}/planner-migration:latest
profiles: [migrate]
backend:
image: ${REGISTRY_HOST}/planner-backend:latest
expose:
- "4000"
frontend:
image: ${REGISTRY_HOST}/planner-frontend:latest
expose:
- "80"
website:
image: ${REGISTRY_HOST}/planner-website:latest
expose:
- "8080"
docs:
image: ${REGISTRY_HOST}/planner-docs:latestMigration Runner
A lightweight Docker image for running Prisma migrations and seeding in production.
# Run migrations
docker compose --profile migrate run --rm migration
# Seed after migrate
docker compose --profile migrate run --rm migration seedThe migration image (docker/Dockerfile.migration) is based on node:22-alpine with prisma, tsx, and dotenv. Entrypoint (docker/entrypoint.sh) supports two commands:
migrate(default) —npx prisma migrate deployseed—npx prisma db seed(runstsx prisma/seed.ts)
The backend also runs prisma migrate deploy on startup as a safety net (idempotent).
Production Deployment
docker compose -f docker-compose.prod.yml up -dThe production build uses optimized Dockerfiles (Dockerfile.backend.prod) with multi-stage builds for smaller image sizes.
Dockerfiles
All Dockerfiles are based on node:22-alpine with a unified stage naming system:
| Stage | Description |
|---|---|
base | Base Node image |
deps | Install dependencies |
dev | Development mode |
builder | Build application |
runner | Production runtime |
Backend Dockerfile (docker/Dockerfile.backend)
FROM node:22-alpine AS base
# ... standard pnpm setup with corepack
WORKDIR /app
FROM base AS deps
COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
FROM base AS dev
COPY --from=deps /app/node_modules ./node_modules
COPY . .
CMD ["sh", "-c", "cd apps/backend && pnpm dev"]Environment Variables
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | postgresql://planner:planner@localhost:5432/planner | PostgreSQL connection |
JWT_SECRET | (required) | Secret for JWT signing |
REDIS_PASSWORD | planner_redis | Redis password |
SMTP_HOST | (optional) | SMTP server for emails |
SMTP_PORT | 1025 | SMTP port |
SMTP_USER | (optional) | SMTP username |
SMTP_PASS | (optional) | SMTP password |
SMTP_FROM | noreply@planner.app | Sender email address |
CORS_ORIGINS | (required) | Allowed origins (comma-separated) |
UPLOAD_DIR | ./uploads | File upload directory |
Security
HTTPS / SSL
Production deployment uses Caddy reverse proxy with automatic Let's Encrypt SSL. Port 80 must be reachable for domain validation. All traffic is encrypted with TLSv1.2/TLSv1.3.
Security Headers
- CSP: Content Security Policy restricts script, style, font, image, and connection sources
- HSTS:
max-age=31536000; includeSubDomains; preload(production only) - X-Content-Type-Options:
nosniffon all responses - CORS: Whitelist via
CORS_ORIGINSenv var,credentials: true
Rate Limiting
- Global: 60 requests/minute via Redis
- Per-endpoint: register (5/min), login (10/min), refresh (20/min), forgot-password (3/min), reset-password (5/min), verify-email (5/min), resend-verification (3/min)
- Account lockout: 5 failed login attempts → 15-minute block
File Uploads
- Max size: 50MB (documents), 2MB (avatars)
- Magic bytes validation (JPEG, PNG, PDF, etc.)
- SVG files blocked (XSS risk)
- Documents served with
Content-Disposition: attachment - S3 ACL: avatars
public-read, documentsprivate
API Documentation
Swagger UI is available at /api/docs in development only. Disabled in production (NODE_ENV === 'production').
Port Mapping
| Service | Internal Port | External Port |
|---|---|---|
| Backend | 4000 | 4000 |
| Frontend | 3000 | 3000 |
| Website | 8080 | 8080 |
| Documentation | 8080 | 8081 |
| PostgreSQL | 5432 | 5432 |
Health Checks
All services respond with HTTP 200 when running:
curl http://localhost:4000/api/health
curl http://localhost:3000
curl http://localhost:8080
curl http://localhost:8081The health endpoint checks DB, Redis, LiveKit, and S3 connectivity. Error details are hidden in production.