Skip to content

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.

yaml
# 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: host

Production (docker-compose.prod.yml) — apps only

Infrastructure uses external managed services (RDS, ElastiCache, etc.).

yaml
# 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:latest

Migration Runner

A lightweight Docker image for running Prisma migrations and seeding in production.

bash
# Run migrations
docker compose --profile migrate run --rm migration

# Seed after migrate
docker compose --profile migrate run --rm migration seed

The 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 deploy
  • seednpx prisma db seed (runs tsx prisma/seed.ts)

The backend also runs prisma migrate deploy on startup as a safety net (idempotent).

Production Deployment

bash
docker compose -f docker-compose.prod.yml up -d

The 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:

StageDescription
baseBase Node image
depsInstall dependencies
devDevelopment mode
builderBuild application
runnerProduction runtime

Backend Dockerfile (docker/Dockerfile.backend)

dockerfile
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

VariableDefaultDescription
DATABASE_URLpostgresql://planner:planner@localhost:5432/plannerPostgreSQL connection
JWT_SECRET(required)Secret for JWT signing
REDIS_PASSWORDplanner_redisRedis password
SMTP_HOST(optional)SMTP server for emails
SMTP_PORT1025SMTP port
SMTP_USER(optional)SMTP username
SMTP_PASS(optional)SMTP password
SMTP_FROMnoreply@planner.appSender email address
CORS_ORIGINS(required)Allowed origins (comma-separated)
UPLOAD_DIR./uploadsFile 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: nosniff on all responses
  • CORS: Whitelist via CORS_ORIGINS env 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, documents private

API Documentation

Swagger UI is available at /api/docs in development only. Disabled in production (NODE_ENV === 'production').

Port Mapping

ServiceInternal PortExternal Port
Backend40004000
Frontend30003000
Website80808080
Documentation80808081
PostgreSQL54325432

Health Checks

All services respond with HTTP 200 when running:

bash
curl http://localhost:4000/api/health
curl http://localhost:3000
curl http://localhost:8080
curl http://localhost:8081

The health endpoint checks DB, Redis, LiveKit, and S3 connectivity. Error details are hidden in production.