DevOps & Deployment
Overview
Deployment and observability ensure applications run reliably in production. This skill covers deployment strategies, CI/CD automation, environment management, and monitoring.
Why it matters:
- •Fast, reliable deploys enable shipping features safely
- •Monitoring catches issues before users notice
- •Environment consistency prevents "works on my machine" bugs
- •Automated pipelines reduce human error
Core Concepts
1. Deployment Environments
typescript
// Define environment-specific configurations
interface EnvironmentConfig {
name: "development" | "staging" | "production";
apiUrl: string;
debugLogging: boolean;
errorTracking: boolean;
cacheEnabled: boolean;
}
// Environment-specific values
const environments: Record<string, EnvironmentConfig> = {
development: {
name: "development",
apiUrl: "http://localhost:3001",
debugLogging: true,
errorTracking: false,
cacheEnabled: false,
},
staging: {
name: "staging",
apiUrl: "https://staging-api.example.com",
debugLogging: true,
errorTracking: true,
cacheEnabled: true,
},
production: {
name: "production",
apiUrl: "https://api.example.com",
debugLogging: false,
errorTracking: true,
cacheEnabled: true,
},
};
// Load environment at runtime
const config = environments[process.env.NODE_ENV || "development"];
// Use in code
const apiUrl = config.apiUrl;
2. Environment Variables
bash
# .env.local (NEVER COMMIT) DATABASE_URL=postgresql://user:pass@localhost/db API_KEY_SECRET=sk_test_abc123 JWT_SECRET=your-super-secret-key DEBUG=true # .env.production (for Vercel, use their UI instead) # In Vercel: Settings → Environment Variables
typescript
// In code const dbUrl = process.env.DATABASE_URL; const apiKey = process.env.API_KEY_SECRET; // For client-side (must be prefixed with NEXT_PUBLIC_) const PUBLIC_API_URL = process.env.NEXT_PUBLIC_API_URL;
3. CI/CD Pipeline (GitHub Actions Example)
yaml
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
test_and_build:
runs-on: ubuntu-latest
steps:
# Check out code
- uses: actions/checkout@v3
# Setup Node
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: "pnpm"
# Install dependencies
- run: pnpm install
# Run tests
- run: pnpm test
- run: pnpm lint
- run: pnpm build
# Deploy to Vercel
- name: Deploy to Vercel
uses: vercel/action@main
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
4. Zero-Downtime Deployments
typescript
// Strategy: Blue-Green Deployment
// 1. Keep current version running (Blue)
// 2. Deploy new version to separate instance (Green)
// 3. Test new version thoroughly
// 4. Switch traffic from Blue → Green
// Vercel does this automatically:
// - New deployment gets unique preview URL
// - Test thoroughly (staging environment)
// - Promote to production (instant switch)
// Implementation: Graceful shutdown
async function shutdown(signal: string) {
console.log(`Received ${signal}, shutting down gracefully...`);
// Stop accepting new requests
server.close(() => {
console.log("Server closed");
process.exit(0);
});
// Wait up to 30 seconds for existing requests to finish
setTimeout(() => {
console.log("Force closing outstanding connections");
process.exit(1);
}, 30000);
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
Deep Patterns
Pattern 1: Database Migrations (Prisma)
typescript
// Generate migration when schema changes // Command: pnpm prisma migrate dev --name add_user_email // Migration files are version-controlled // migrations/ // ├── 20240213100000_initial/ // │ └── migration.sql // └── 20240213120000_add_user_email/ // └── migration.sql // In CI/CD before deploy: // pnpm prisma migrate deploy // Applies all pending migrations to production database // Workflow for schema changes: // 1. Update schema.prisma // 2. Run: pnpm prisma migrate dev --name describe_change // 3. Review migration file // 4. Commit migration (version controlled) // 5. CI/CD applies on deploy
Pattern 2: Secrets Management
typescript
// ❌ WRONG: Store secrets in git DATABASE_PASSWORD = "super_secret_123" # In .env file // ✅ RIGHT: Use Vercel's secret management // 1. In Vercel dashboard: Settings → Environment Variables // 2. Add SECRET_NAME: "value" // 3. In code: process.env.SECRET_NAME // ✅ BETTER: Use external secret manager (Vercel + GitHub) // 1. Generate on production service // 2. Only accessible to authorized applications // 3. Rotated regularly // 4. Never logged or exposed // For local development: // 1. Create .env.local (add to .gitignore) // 2. Only you have these secrets // 3. Never commit them
Pattern 3: Health Checks & Monitoring
typescript
// Health check endpoint
app.get("/health", (req, res) => {
const health = {
status: "ok",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV,
database: "checking...",
};
// Quick database check
db.query("SELECT 1")
.then(() => {
health.database = "connected";
res.status(200).json(health);
})
.catch(() => {
health.database = "disconnected";
res.status(503).json(health);
});
});
// Vercel: Configure health checks in vercel.json
// {
// "env": { "HEALTH_CHECK_PATH": "/health" }
// }
Pattern 4: Logging & Observability
typescript
// Structured logging (not console.log!)
class Logger {
info(message: string, meta?: Record<string, unknown>) {
console.log(
JSON.stringify({
level: "info",
message,
timestamp: new Date().toISOString(),
...meta,
}),
);
}
error(message: string, error?: Error, meta?: Record<string, unknown>) {
console.error(
JSON.stringify({
level: "error",
message,
error: error?.message,
stack: error?.stack,
timestamp: new Date().toISOString(),
...meta,
}),
);
}
}
// Usage
const logger = new Logger();
logger.info("User logged in", { userId: "123", loginMethod: "google" });
logger.error("Database connection failed", error, { retries: 3 });
// In production + observability tool (Sentry, Datadog, etc.):
// - Logs captured automatically
// - Can search and filter by any field
// - Alerts on error rates
// - Performance metrics tracked
Pattern 5: Feature Flags (Safe Rollouts)
typescript
// Deploy code that's not yet active
function isFeatureEnabled(feature: string, userId?: string): boolean {
// Example: 10% of users see new feature
if (feature === 'new-search') {
if (userId) {
return hashUserId(userId) % 100 < 10; // 10% of users
}
return false;
}
// Example: Feature only in staging
if (feature === 'experimental-api') {
return process.env.NODE_ENV === 'staging';
}
return false;
}
// Usage
export function SearchBox() {
const useNewSearch = isFeatureEnabled('new-search');
if (useNewSearch) {
return <NewSearchComponent />;
} else {
return <LegacySearchComponent />;
}
}
// Benefit: Deploy, test with 10%, expand to 50%, then 100%
// If issues arise, rollback instantly by disabling flag
Deployment Checklist
Before shipping to production:
- • All tests passing (pnpm test)
- • No lint errors (pnpm lint)
- • Build succeeds (pnpm build)
- • Environment variables configured in Vercel
- • Database migrations reviewed and tested
- • Feature flags tested in staging
- • Secrets not exposed anywhere
- • Health checks passing
- • Monitoring/alerting configured
- • Rollback plan documented
Vercel-Specific Best Practices
typescript
// vercel.json - Configure Vercel
{
"buildCommand": "pnpm run build",
"installCommand": "pnpm install",
"env": {
"NODE_ENV": "production"
},
"regions": ["sfo1"], // Deploy in specific region
"functions": {
"api/**": {
"memory": 1024,
"maxDuration": 60
}
}
}
// Preview deployments
// - Every push creates preview.example.com URL
// - Full production-like environment
// - Test before deploying to main domain
// - Share with team for review
// Production deployment
// - Automatic on merge to main branch
// - Instant propagation to CDN
// - Built-in analytics and monitoring
Anti-Patterns to Avoid
typescript
// ❌ Wrong: Storing secrets in code const API_KEY = "sk_live_abc123"; // In source file // ✅ Correct: Load from environment const API_KEY = process.env.API_KEY; // ❌ Wrong: No database backups before migration // Schema change → data loss if migration fails // ✅ Correct: Backups + test migrations first // 1. Backup production database // 2. Test migration on backup // 3. Run migration on production // ❌ Wrong: Deploy without tests // Ship untested code → production bugs // ✅ Correct: CI/CD runs tests before deploy // Tests → Build → Deploy // ❌ Wrong: No monitoring after deploy // Bugs discovered by angry users // ✅ Correct: Monitor errors, performance, uptime // Alerts notify you in Slack
Monitoring & Alerting Strategy
| Metric | Tool | Alert Threshold |
|---|---|---|
| Error Rate | Sentry | > 1% errors |
| Response Time | Vercel Analytics | > 2s P95 |
| Uptime | Pingdom | < 99.5% |
| Memory Usage | Vercel | > 512MB |
| Login Success | Sentry | < 95% |
Key Questions
- •How do I safely deploy changes? (CI/CD pipeline)
- •What if something breaks in production? (Rollback plan)
- •How do users get new features? (Feature flags, gradual rollout)
- •What metrics matter? (Define SLOs)
- •Are my secrets safe? (Environment variables, secret manager)