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:6379CI/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-prodResource 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.jsonturbo.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:/dataSet S3_ENDPOINT=http://minio:9000 in local env. The same @aws-sdk/client-s3 works against MinIO unchanged.