Infra: Overview

All running processes

Process Type Port Who writes it
api-gateway App (TS) 3000 You
core-api App (TS) 4000 You
tenant-web App (Next.js) 3001 You
notification-service App (TS) 4001 (health only) You
ai-worker App (Python) 4002 (health only) You
postgres Infrastructure 5432 docker run postgres
redis Infrastructure 6379 docker run redis

docker-compose.yml (local dev)

version: '3.9'
 
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: placement
      POSTGRES_PASSWORD: placement
      POSTGRES_DB: placement_dev
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U placement"]
      interval: 5s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
 
  api-gateway:
    build:
      context: ./apps/api-gateway
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      PORT: 3000
      CORE_API_URL: http://core-api:4000
      REDIS_URL: redis://redis:6379
    depends_on:
      redis:
        condition: service_healthy
      core-api:
        condition: service_healthy
    volumes:
      - ./apps/api-gateway/src:/app/src  # hot reload in dev
 
  core-api:
    build:
      context: ./apps/core-api
      dockerfile: Dockerfile
    ports:
      - "4000:4000"
    environment:
      PORT: 4000
      DATABASE_URL: postgresql://placement:placement@postgres:5432/placement_dev
      REDIS_URL: redis://redis:6379
      S3_BUCKET: placement-local
      # Add all other env vars
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - ./apps/core-api/src:/app/src
 
  tenant-web:
    build:
      context: ./apps/tenant-web
      dockerfile: Dockerfile
    ports:
      - "3001:3001"
    environment:
      NEXT_PUBLIC_API_URL: http://localhost:3000/api
    depends_on:
      - api-gateway
 
  notification-service:
    build:
      context: ./apps/notification-service
      dockerfile: Dockerfile
    ports:
      - "4001:4001"
    environment:
      REDIS_URL: redis://redis:6379
      DATABASE_URL: postgresql://placement:placement@postgres:5432/placement_dev
      RESEND_API_KEY: re_...
    depends_on:
      redis:
        condition: service_healthy
      postgres:
        condition: service_healthy
 
  ai-worker:
    build:
      context: ./apps/ai-worker
      dockerfile: Dockerfile
    ports:
      - "4002:4002"
    environment:
      REDIS_URL: redis://redis:6379
      DATABASE_URL: postgresql://placement:placement@postgres:5432/placement_dev
      OPENAI_API_KEY: sk-...
      CORE_API_INTERNAL_URL: http://core-api:4000
      CORE_API_INTERNAL_SECRET: dev-secret
    depends_on:
      redis:
        condition: service_healthy
      core-api:
        condition: service_healthy
 
volumes:
  postgres_data:
  redis_data:

Kubernetes layout (production)

One cluster, multiple namespaces:

cluster/
├── namespace: placement-prod
│   ├── Deployment: api-gateway       (2–8 replicas)
│   ├── Deployment: core-api          (2–10 replicas)
│   ├── Deployment: tenant-web        (2–4 replicas)
│   ├── Deployment: notification-svc  (1–4 replicas)
│   ├── Deployment: ai-worker         (1–6 replicas)
│   ├── Service: api-gateway          (ClusterIP)
│   ├── Service: core-api             (ClusterIP)
│   ├── Ingress: nginx
│   │   ├── *.platform.com → api-gateway:3000
│   │   └── app.platform.com → tenant-web:3001
│   └── HPA: (see below)
├── namespace: placement-staging
│   └── (same, lower replicas)
└── namespace: infra
    └── (Redis standalone or use managed)

Postgres and Redis use managed services in production (RDS/Neon for Postgres, Upstash/ElastiCache for Redis). Do not run stateful services in k8s.


HPA (Horizontal Pod Autoscaler) configs

# api-gateway HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-gateway-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-gateway
  minReplicas: 2
  maxReplicas: 8
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60
 
# ai-worker HPA — scale on queue depth
# Requires KEDA (Kubernetes Event-Driven Autoscaling) for queue-based scaling
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: ai-worker-scaler
spec:
  scaleTargetRef:
    name: ai-worker
  minReplicaCount: 1
  maxReplicaCount: 6
  triggers:
    - type: redis
      metadata:
        listName: "bull:ai-jobs:wait"
        listLength: "5"    # scale up if more than 5 jobs waiting
        address: redis:6379

CI/CD pipeline (GitHub Actions)

# .github/workflows/deploy.yml
 
on:
  push:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_PASSWORD: test, POSTGRES_DB: placement_test }
      redis:
        image: redis:7
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npx turbo test
 
  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service: [api-gateway, core-api, tenant-web, notification-service, ai-worker]
    steps:
      - uses: actions/checkout@v4
      - name: Build and push Docker image
        run: |
          docker build -t $ECR_REGISTRY/placement-${{ matrix.service }}:$GITHUB_SHA \
            ./apps/${{ matrix.service }}
          docker push $ECR_REGISTRY/placement-${{ matrix.service }}:$GITHUB_SHA
 
  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to k8s via Helm
        run: |
          helm upgrade --install placement ./infra/helm \
            --set image.tag=$GITHUB_SHA \
            --namespace placement-prod

Resource requests/limits per pod

# api-gateway, core-api, tenant-web
resources:
  requests:
    cpu: "100m"
    memory: "256Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"
 
# notification-service
resources:
  requests:
    cpu: "50m"
    memory: "128Mi"
  limits:
    cpu: "200m"
    memory: "256Mi"
 
# ai-worker (LLM calls are I/O bound, not CPU bound)
resources:
  requests:
    cpu: "100m"
    memory: "256Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"

Local dev without Docker

For fast iteration, run just the infra in Docker and run apps natively:

# Start only infra
docker-compose up postgres redis
 
# Run apps natively (each in a separate terminal or using Turborepo)
npx turbo dev
# This runs `dev` script in each app concurrently via turbo.json

turbo.json:

{
  "pipeline": {
    "dev": {
      "cache": false,
      "persistent": true
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    }
  }
}

S3 local development

Use MinIO as a local S3-compatible store:

# Add to docker-compose.yml for local dev
minio:
  image: minio/minio
  ports:
    - "9000:9000"
    - "9001:9001"   # console
  environment:
    MINIO_ROOT_USER: placement
    MINIO_ROOT_PASSWORD: placement123
  command: server /data --console-address ":9001"
  volumes:
    - minio_data:/data

Set S3_ENDPOINT=http://minio:9000 in local env. The same @aws-sdk/client-s3 works against MinIO unchanged.