deployment
Resources
scripts/
validate-deployment.sh
references/
deployment-platforms.md
Deployment
This skill guides you through deploying applications to production using modern platforms and tools. Use this workflow when deploying Next.js, full-stack apps, containerized services, or serverless functions.
When to Use This Skill
- Deploying applications to Vercel, Railway, Fly.io, or AWS
- Setting up CI/CD pipelines with GitHub Actions
- Configuring Docker containers for production
- Implementing health checks and monitoring
- Creating preview deployments for pull requests
- Setting up rollback and canary deployment strategies
Platform Selection
Choose the right platform based on your application needs:
Vercel (Best for Next.js)
When to use:
- Next.js applications (App Router or Pages Router)
- Static sites with edge functions
- Automatic preview deployments per PR
- Zero-config deployments
Setup:
# Install Vercel CLI
npm install -g vercel
# Deploy
vercel --prod
Environment variables:
# Set production secrets
vercel env add DATABASE_URL production
vercel env add NEXTAUTH_SECRET production
Railway (Best for Full-Stack)
When to use:
- Full-stack apps with databases
- Monorepo deployments
- PostgreSQL, Redis, MongoDB hosting
- WebSocket support
Setup:
# Install Railway CLI
npm install -g @railway/cli
# Login and deploy
railway login
railway up
railway.json:
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS",
"buildCommand": "npm run build"
},
"deploy": {
"startCommand": "npm start",
"healthcheckPath": "/api/health",
"healthcheckTimeout": 100,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}
Fly.io (Best for Containers)
When to use:
- Custom container requirements
- Global edge deployment
- Long-running processes
- Fine-grained scaling control
Setup:
# Install Fly CLI
curl -L https://fly.io/install.sh | sh
# Initialize and deploy
fly launch
fly deploy
fly.toml:
app = "my-app"
primary_region = "sjc"
[build]
dockerfile = "Dockerfile"
[env]
PORT = "8080"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
[[http_service.checks]]
grace_period = "10s"
interval = "30s"
method = "GET"
timeout = "5s"
path = "/api/health"
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 256
Docker (Best for Self-Hosted)
When to use:
- Self-hosted infrastructure
- VPS deployments (DigitalOcean, Linode)
- Local development parity
- Multi-service orchestration
See references/deployment-platforms.md for Dockerfile examples.
AWS (Best for Enterprise)
When to use:
- Enterprise requirements
- Compliance/regulatory needs
- Complex infrastructure
- Multi-region deployments
Services:
- ECS Fargate: Serverless containers
- Lambda: Serverless functions
- Amplify: Full-stack deployments
- Elastic Beanstalk: Managed platform
Environment Configuration
.env Management
Never commit secrets to git. Always use .env.example for documentation:
.env.example:
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
# Authentication
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="generate-with-openssl-rand-base64-32"
# External APIs
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
# Feature Flags
NEXT_PUBLIC_ENABLE_ANALYTICS="false"
Environment Variable Validation
Validate environment variables at build time to fail fast:
src/env.mjs:
import { z } from 'zod';
const server = z.object({
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
});
const client = z.object({
NEXT_PUBLIC_APP_URL: z.string().url(),
});
const processEnv = {
DATABASE_URL: process.env.DATABASE_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
};
const merged = server.merge(client);
const parsed = merged.safeParse(processEnv);
if (!parsed.success) {
console.error('[FAIL] Invalid environment variables:', parsed.error.flatten().fieldErrors);
throw new Error('Invalid environment variables');
}
export const env = parsed.data;
Import at the top of your app to validate on startup:
import { env } from './env.mjs';
// Use typed, validated env
const db = new PrismaClient({
datasources: { db: { url: env.DATABASE_URL } },
});
CI/CD Pipelines
GitHub Actions Workflow
Create .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
SKIP_ENV_VALIDATION: true
deploy:
runs-on: ubuntu-latest
needs: [build]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Caching Strategies
Speed up CI/CD with proper caching:
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.npm
.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
${{ runner.os }}-nextjs-
Docker Production Setup
Multi-Stage Dockerfile
Create an optimized production Dockerfile:
Dockerfile (Next.js):
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
next.config.js:
module.exports = {
output: 'standalone', // Required for Docker
};
.dockerignore
Exclude unnecessary files from the Docker build:
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
.gitignore
.env*.local
.vscode
.idea
dist
build
coverage
*.md
!README.md
Build and Run
# Build the image
docker build -t my-app .
# Run with environment variables
docker run -p 3000:3000 \
-e DATABASE_URL="postgresql://..." \
-e NEXTAUTH_SECRET="..." \
my-app
Health Checks
Health Check Endpoint
Create a health check endpoint for monitoring:
app/api/health/route.ts:
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET() {
try {
// Check database connection
await prisma.$queryRaw`SELECT 1`;
return NextResponse.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
database: 'connected',
});
} catch (error) {
return NextResponse.json(
{
status: 'unhealthy',
timestamp: new Date().toISOString(),
database: 'disconnected',
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 503 }
);
}
}
Docker Health Check
Add health check to Dockerfile:
HEALTHCHECK \
CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
Kubernetes Liveness/Readiness
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
Preview Deployments
Automatic PR Previews
Vercel and Railway automatically create preview deployments for pull requests.
GitHub Actions for Railway:
on:
pull_request:
branches: [main]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Railway (PR)
uses: bervProject/railway-deploy@main
with:
railway_token: ${{ secrets.RAILWAY_TOKEN }}
service: ${{ secrets.RAILWAY_SERVICE }}
Comment on PR with Preview URL
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '[DEPLOY] Preview deployed to: https://pr-${{ github.event.number }}.myapp.com'
})
Rollback Strategies
Instant Rollback (Vercel)
# List deployments
vercel ls
# Promote a previous deployment to production
vercel promote <deployment-url>
Blue-Green Deployment
Deploy new version alongside old, then switch traffic:
# Deploy new version (green)
fly deploy --strategy bluegreen
# Traffic switches automatically after health checks pass
# Rollback if needed:
fly releases rollback
Canary Deployment
Gradually shift traffic to new version:
Fly.io canary:
# Deploy canary (10% traffic)
fly deploy --strategy canary
# Promote to 100% if successful
fly releases promote
Monitoring and Error Tracking
Sentry Integration
npm install @sentry/nextjs
sentry.client.config.ts:
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV,
enabled: process.env.NODE_ENV === 'production',
});
sentry.server.config.ts:
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV,
});
Uptime Monitoring
Use external services to monitor availability:
- Better Uptime: https://betteruptime.com
- Pingdom: https://www.pingdom.com
- UptimeRobot: https://uptimerobot.com
Monitor your /api/health endpoint every 1-5 minutes.
Log Aggregation
Vercel:
- Built-in log streaming
- Integration with Datadog, LogDNA, Axiom
Railway:
- Built-in logs in dashboard
- Export to external services
Self-hosted:
# Use Docker logging driver
docker run --log-driver=json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
my-app
Database Migrations in CI/CD
Prisma Migrations
Run migrations before deployment:
GitHub Actions:
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Railway: Add to railway.json:
{
"deploy": {
"startCommand": "npx prisma migrate deploy && npm start"
}
}
Migration Safety
Never run destructive migrations automatically:
-
Backwards compatible migrations first
- Add new columns as nullable
- Deploy code that works with old and new schema
- Run migration
- Deploy code that requires new schema
- Remove old columns in future migration
-
Manual approval for production
- name: Run migrations if: github.event_name == 'workflow_dispatch' run: npx prisma migrate deploy
Precision Tool Integration
Validate Deployment with precision_exec
Use precision_exec to run deployment commands with expectations:
precision_exec:
commands:
- cmd: "npm run build"
expect:
exit_code: 0
- cmd: "docker build -t my-app ."
expect:
exit_code: 0
- cmd: "npm run typecheck"
expect:
exit_code: 0
verbosity: minimal
Health Check with precision_fetch
Validate deployment health:
precision_fetch:
requests:
- url: "https://my-app.com/api/health"
method: GET
expect:
status: 200
body_contains: '"status":"healthy"'
Discover Deployment Gaps
Before deploying, check for missing configuration:
discover:
queries:
- id: env_example
type: glob
patterns: [".env.example"]
- id: dockerfile
type: glob
patterns: ["Dockerfile", "docker-compose.yml"]
- id: ci_config
type: glob
patterns: [".github/workflows/*.yml"]
- id: health_check
type: grep
pattern: '/api/health|/health'
glob: "**/*.{ts,tsx,js,jsx}"
output_mode: count_only
Pre-Deployment Checklist
Run the validation script:
./plugins/goodvibes/skills/outcome/deployment/scripts/validate-deployment.sh /path/to/project
The script checks:
- Dockerfile exists
- .env.example exists and documents required variables
- CI/CD configuration present
- Health check endpoint implemented
- .dockerignore exists
- No hardcoded secrets in code
- Build command succeeds
- Database migration configuration present
Common Pitfalls
1. Missing Environment Variables
Problem: Deployment fails because environment variables aren't set.
Solution: Document all variables in .env.example and validate at build time with zod.
2. Database Connection Pooling
Problem: Serverless functions exhaust database connections.
Solution: Use connection pooling (PgBouncer, Prisma Accelerate, Supabase pooler).
// Use connection pooler in serverless
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL, // Use pooled connection string
},
},
});
3. Build Output Not Optimized
Problem: Large Docker images, slow cold starts.
Solution: Use multi-stage builds, standalone output for Next.js, proper .dockerignore.
4. Migrations Run on Every Deploy
Problem: Prisma migrations run on every container start.
Solution: Separate migration step from app startup in CI/CD.
5. No Rollback Plan
Problem: Bad deployment breaks production with no easy fix.
Solution: Use platforms with instant rollback (Vercel, Railway, Fly.io) or maintain previous Docker images.
Summary
Key Principles:
- Validate environment variables at build time - Fail fast, not in production
- Automate everything - CI/CD should handle lint, test, build, deploy
- Health checks are mandatory - Every service needs a health endpoint
- Preview deployments for every PR - Catch issues before merging
- Always have a rollback plan - Instant rollback > fixing forward
- Monitor from day one - Error tracking and uptime monitoring are not optional
- Migrations are dangerous - Run them carefully with backwards compatibility
Next Steps:
- Run
validate-deployment.shon your project - Set up CI/CD pipeline with GitHub Actions
- Configure environment variables in your platform
- Add health check endpoint
- Test deployment to staging environment
- Deploy to production
- Set up monitoring and alerting
For detailed platform configurations and templates, see references/deployment-platforms.md.