pnpm monorepo · Turborepo · Docker · GitHub Actions CI/CD · Horizontal scaling
v7.0 · April 2026
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
First-time setup through to running individual services
Installs all workspace packages and links internal deps via pnpm hoisting.
Turborepo starts @teezu/web (Vite, port 5173) and @teezu/api (Fastify, port 3001) in parallel, with hot-reload on both.
# Web app only pnpm --filter @teezu/web dev # API only pnpm --filter @teezu/api dev # Shared types (watch mode) pnpm --filter @teezu/types dev
# Check all packages pnpm v3:type-check # Check a single package pnpm --filter @teezu/api type-check
# 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
Turborepo builds packages in dependency order with remote cache support
pnpm install --frozen-lockfile pnpm v3:build
Turborepo resolves the task graph: @teezu/types → @teezu/ui → @teezu/web + @teezu/api in parallel.
apps/web/dist/ # Static assets (Vite build) apps/api/dist/ # Compiled Fastify server (tsc) packages/types/dist/ # Compiled Zod schemas + TS types
{
"$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/**"]
}
}
}
Local dev via docker-compose · production via multi-stage Dockerfile per service
docker-compose up -d
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"]
Store secrets in CI/CD secrets manager — never commit .env files
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
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
GitHub Actions — test → type-check → build → Docker push → deploy
pnpm v3:type-check — ensures no TypeScript errors in any packagepnpm v3:build via Turborepo with remote cachedrizzle-kit migrate against production DB before new pods startGET /health 200name: 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
Stateless API pods · Redis for shared state · Drizzle connection pooling
messages table (future)# 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
# 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
# Full production build pnpm install --frozen-lockfile pnpm v3:build # Build single service pnpm --filter @teezu/api build
# 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
pnpm v3:type-check cleanGET /health returns 200 on all pods/metrics