🚢

Deployment Guide

pnpm monorepo · Turborepo · Docker · GitHub Actions CI/CD · Horizontal scaling

v7.0 · April 2026

🏗️ Monorepo Structure

pnpm workspaces + Turborepo for task orchestration and incremental builds

TeezU/
├── apps/
│   ├── web/          # @teezu/web   (Vite + React)
│   └── api/          # @teezu/api   (Fastify 5)
├── packages/
│   ├── types/        # @teezu/types  (Zod schemas, shared types)
│   ├── ui/           # @teezu/ui     (component library)
│   └── config/       # @teezu/config (tsconfig, eslint)
├── pnpm-workspace.yaml
├── turbo.json
└── package.json
Package Manager
pnpm 9
Strict workspace isolation, symlink node_modules
Build Orchestration
Turborepo
Dependency-aware task graph, remote caching, parallel execution
Shared Types
@teezu/types
Zod schemas compiled once, consumed by both web and api

💻 Local Development

First-time setup through to running individual services

1 — Install all dependencies

pnpm install

Installs all workspace packages and links internal deps via pnpm hoisting.

2 — Start all services (Turborepo dev)

pnpm v3:dev

Turborepo starts @teezu/web (Vite, port 5173) and @teezu/api (Fastify, port 3001) in parallel, with hot-reload on both.

Run individual services

# Web app only
pnpm --filter @teezu/web dev

# API only
pnpm --filter @teezu/api dev

# Shared types (watch mode)
pnpm --filter @teezu/types dev

Type checking

# Check all packages
pnpm v3:type-check

# Check a single package
pnpm --filter @teezu/api type-check

Database sync (Drizzle)

# Push schema changes to the local DB (no migration file generated)
pnpm --filter @teezu/api db:push

# Generate migration SQL file from schema diff
pnpm --filter @teezu/api db:generate

# Apply pending migrations
pnpm --filter @teezu/api db:migrate

# Open Drizzle Studio (visual DB browser)
pnpm --filter @teezu/api db:studio

📦 Production Build

Turborepo builds packages in dependency order with remote cache support

Full monorepo build

pnpm install --frozen-lockfile
pnpm v3:build

Turborepo resolves the task graph: @teezu/types@teezu/ui@teezu/web + @teezu/api in parallel.

Build outputs

apps/web/dist/      # Static assets (Vite build)
apps/api/dist/      # Compiled Fastify server (tsc)
packages/types/dist/ # Compiled Zod schemas + TS types

turbo.json task pipeline

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "type-check": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    }
  }
}

🐳 Docker

Local dev via docker-compose · production via multi-stage Dockerfile per service

Local dev stack (docker-compose)

docker-compose up -d
  • @teezu/web — Vite dev server · port 5173
  • @teezu/api — Fastify 5 · port 3001
  • PostgreSQL 16 — primary DB · port 5432
  • Redis 7 — session store / rate-limit / pubsub · port 6379
  • Nginx — reverse proxy → /api → api, / → web · ports 80 / 443

Production multi-stage Dockerfile (API)

FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate

# ── deps stage ────────────────────────────────────
FROM base AS deps
WORKDIR /repo
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
COPY packages/ ./packages/
COPY apps/api/package.json ./apps/api/
RUN pnpm install --frozen-lockfile --filter @teezu/api...

# ── build stage ───────────────────────────────────
FROM deps AS builder
COPY . .
RUN pnpm v3:build --filter @teezu/api...

# ── runtime stage ─────────────────────────────────
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /repo/apps/api/dist ./dist
COPY --from=builder /repo/apps/api/package.json .
COPY --from=deps    /repo/node_modules   ./node_modules
EXPOSE 3001
CMD ["node", "dist/index.js"]

⚙️ Environment Variables

Store secrets in CI/CD secrets manager — never commit .env files

apps/api/.env

NODE_ENV=production
PORT=3001

# Database (PostgreSQL + Drizzle)
DATABASE_URL=postgresql://user:pass@db:5432/teezu

# Redis (session store, rate-limit, Socket.IO adapter)
REDIS_URL=redis://redis:6379

# Lucia Auth
LUCIA_SECRET=change_me_32_chars_min

# JWT fallback (Bearer token signing)
JWT_SECRET=change_me_another_secret

# Media storage
S3_BUCKET=teezu-media
S3_REGION=eu-west-1
S3_ACCESS_KEY=AKIAxxxxxxxxxxxxxxxx
S3_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CDN_BASE_URL=https://cdn.teezu.online

# AI service
OPENAI_API_KEY=sk-...
AI_MODERATION_THRESHOLD=0.8

# Sentry
SENTRY_DSN=https://...@o0.ingest.sentry.io/0

apps/web/.env

VITE_API_URL=https://api.teezu.online/api/v1
VITE_WS_URL=wss://api.teezu.online
VITE_CDN_URL=https://cdn.teezu.online
VITE_APP_VERSION=7.0.0

🔄 CI/CD Pipeline

GitHub Actions — test → type-check → build → Docker push → deploy

Pipeline stages

1
Test
Run Vitest unit + integration tests across all packages
2
Type Check
pnpm v3:type-check — ensures no TypeScript errors in any package
3
Build
pnpm v3:build via Turborepo with remote cache
4
Docker Build & Push
Multi-stage images pushed to GitHub Container Registry (ghcr.io)
5
Database Migrate
Run drizzle-kit migrate against production DB before new pods start
6
Deploy & Health Check
Rolling deploy to Kubernetes / VPS; wait for GET /health 200

.github/workflows/deploy.yml

name: Test → Build → Deploy
on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_API: ghcr.io/${{ github.repository }}/api
  IMAGE_WEB: ghcr.io/${{ github.repository }}/web

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm v3:type-check
      - run: pnpm turbo test

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          file: apps/api/Dockerfile
          push: true
          tags: ${{ env.IMAGE_API }}:latest,${{ env.IMAGE_API }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Run DB migrations
        run: |
          docker run --rm \
            -e DATABASE_URL="${{ secrets.DATABASE_URL }}" \
            ${{ env.IMAGE_API }}:${{ github.sha }} \
            node dist/migrate.js
      - name: Rolling deploy
        run: |
          # kubectl set image deployment/teezu-api api=${{ env.IMAGE_API }}:${{ github.sha }}
          echo "Deploy step — plug in your orchestrator here"
      - name: Health check
        run: curl --retry 5 --retry-delay 5 -f https://api.teezu.online/health

☁️ Cloud Infrastructure

AWS Services

  • EKS / EC2 — Kubernetes cluster for API pods
  • RDS (PostgreSQL 16) — managed primary DB + read replica
  • ElastiCache (Redis 7) — session store, rate-limit, Socket.IO pub/sub
  • S3 — media storage (images, videos, thumbnails)
  • CloudFront — CDN for static assets + S3 media
  • ALB — Application Load Balancer with TLS termination
  • ECR / ghcr.io — container image registry

Monitoring & Observability

  • Sentry — error tracking (frontend + backend)
  • Prometheus + Grafana — metrics & dashboards
  • Pino / structured JSON logs — shipped to CloudWatch
  • Uptime Robot — external health-check alerts
  • GitHub Actions — build / deploy pipeline status

📈 Scaling Strategy

Stateless API pods · Redis for shared state · Drizzle connection pooling

🔄 Horizontal Pod Autoscaling

  • • Kubernetes HPA on CPU ≥ 70%
  • • Min 2 replicas / Max 10 replicas
  • • ALB distributes traffic evenly
  • • Zero-downtime rolling deploys
  • • Stateless design: no local session state

🗃️ Redis Shared State

  • • Lucia session storage (all pods)
  • • @fastify/rate-limit shared counters
  • • Socket.IO Redis adapter (cross-pod emit)
  • • Feed & match result caching
  • • Token balance hot-cache (5 s TTL)

🗄️ Database

  • • PostgreSQL read replica for feed queries
  • • Drizzle connection pool (max 20 per pod)
  • • PgBouncer for connection management
  • • Query result caching via Redis
  • • Partitioning on messages table (future)

🔧 Quick Command Reference

Development

# Install all deps
pnpm install

# Start everything
pnpm v3:dev

# Start single service
pnpm --filter @teezu/web dev
pnpm --filter @teezu/api dev

# Type check all
pnpm v3:type-check

# Run all tests
pnpm turbo test

Database (Drizzle)

# Push schema (no migration file)
pnpm --filter @teezu/api db:push

# Generate migration SQL
pnpm --filter @teezu/api db:generate

# Apply migrations (production)
pnpm --filter @teezu/api db:migrate

# Open Drizzle Studio
pnpm --filter @teezu/api db:studio

Production Build

# Full production build
pnpm install --frozen-lockfile
pnpm v3:build

# Build single service
pnpm --filter @teezu/api build

Docker

# Start local dev stack
docker-compose up -d

# View API logs
docker logs -f teezu-api

# Rebuild + restart API
docker-compose up -d --build api

# Stop everything
docker-compose down

✅ Production Checklist

Pre-Deployment

  • ☑️ All CI tests passing (Vitest)
  • ☑️ pnpm v3:type-check clean
  • ☑️ Drizzle migration SQL reviewed
  • ☑️ Env vars set in secrets manager
  • ☑️ Redis available and reachable
  • ☑️ SSL certificates renewed (> 30 days)
  • ☑️ Docker images built and pushed
  • ☑️ Rollback image tag noted

Post-Deployment

  • ☑️ GET /health returns 200 on all pods
  • ☑️ DB migrations applied successfully
  • ☑️ Socket.IO rooms connecting via Redis adapter
  • ☑️ Auth login flow working end-to-end
  • ☑️ Sentry receiving events
  • ☑️ Prometheus scraping /metrics
  • ☑️ HPA min replicas running
  • ☑️ Rollback procedure validated