When to Use
- Writing or reviewing Dockerfiles
- Setting up GitHub Actions CI/CD workflows
- Configuring Render or Vercel deployments
- Managing environment variables and secrets
- Adding monitoring, health checks, or observability
- Working on
Dockerfile*,.github/workflows/*.yml,render.yaml,vercel.json
Instructions
Step 1: Docker Multi-Stage Build
# Dockerfile — Next.js frontend (multi-stage, non-root, minimal)
FROM node:22-alpine AS base
WORKDIR /app
RUN corepack enable
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
FROM node:22-alpine AS runner
WORKDIR /app
# Non-root user (security requirement)
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000 NODE_ENV=production
CMD ["node", "server.js"]
# Dockerfile.api — FastAPI backend
FROM python:3.11-slim AS builder
WORKDIR /app
RUN pip install uv
COPY pyproject.toml ./
RUN uv pip install --system -e ".[prod]"
FROM python:3.11-slim AS runner
WORKDIR /app
RUN useradd -m -u 1001 appuser
COPY --from=builder /usr/local/lib/python3.11 /usr/local/lib/python3.11
COPY --from=builder /usr/local/bin /usr/local/bin
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Step 2: GitHub Actions CI/CD
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint-and-test-web:
name: Web (Lint + Test + Build)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: "9" }
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm lint:web
- run: pnpm test --run
- run: pnpm build:web
lint-and-test-api:
name: API (Lint + Test)
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/api
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install uv && uv pip install --system -e ".[dev]"
- run: ruff check app tests scripts
- run: mypy app --strict
- run: pytest tests -q --cov=app --cov-fail-under=80
Step 3: Environment Variable Management
# .env.example — committed to git (placeholder values only)
# Copy to .env and fill in real values (NEVER commit .env)
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/bitlysis_dev
# Auth
NEXTAUTH_SECRET=your-secret-here-min-32-chars
NEXTAUTH_URL=http://localhost:3000
# LLM APIs
OPENAI_API_KEY=sk-your-key-here
ANTHROPIC_API_KEY=your-key-here
# Storage
S3_BUCKET_NAME=bitlysis-uploads
S3_REGION=ap-southeast-1
// lib/env.ts — validate at startup, fail fast
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
OPENAI_API_KEY: z.string().startsWith("sk-"),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
});
export const env = envSchema.parse(process.env);
Step 4: Render Deployment (render.yaml)
services:
- type: web
name: bitlysis-api
runtime: python
plan: starter
region: singapore
buildCommand: pip install uv && uv pip install --system -e ".[prod]"
startCommand: uvicorn app.main:app --host 0.0.0.0 --port $PORT
healthCheckPath: /health
envVars:
- key: DATABASE_URL
fromDatabase:
name: bitlysis-db
property: connectionString
- key: OPENAI_API_KEY
sync: false # Set manually in Render dashboard
databases:
- name: bitlysis-db
plan: starter
region: singapore
databaseName: bitlysis
user: bitlysis_user
Step 5: Vercel Configuration (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
]
}
],
"rewrites": [
{ "source": "/api/v1/:path*", "destination": "https://bitlysis-api.onrender.com/api/v1/:path*" }
]
}
Step 6: Health Checks & Observability
# app/routers/health.py
from fastapi import APIRouter
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter()
@router.get("/health")
async def health_check():
"""Liveness probe — is the service running?"""
return {"status": "ok"}
@router.get("/ready")
async def readiness_check(db: AsyncSession = Depends(get_db)):
"""Readiness probe — is the service ready to serve traffic?"""
await db.execute(text("SELECT 1"))
return {"status": "ready", "database": "connected"}
Output Format
For infrastructure changes, provide:
- The updated config file
- Environment variables needed (with
.env.exampleformat) - Verification steps to confirm deployment succeeded
Activate Specialist
@.cursor/.agents/.agents/devinfra-engineer.md