Production Deployment
Production-ready deployment strategies and best practices
Note: This is a reference doc covering generic deployment strategies (blue-green, canary, Kubernetes, etc.). For Izri's actual production setup — Railway with tag-triggered deploys via GitHub Actions — see
railway.md. The sections below are kept for context if the deployment target ever changes.
📋 Overview
This guide covers deploying Izri to production environments, including pre-deployment checklists, deployment strategies, rollback procedures, and monitoring.
✅ Pre-Deployment Checklist
Code & Build
- All tests passing
- Code reviewed and approved
- Dependency vulnerabilities checked (
pnpm audit) - Production build tested locally
- Environment variables documented
- Database migrations ready
- No
.onlyorskipin tests - Console logs removed or gated by log level
Infrastructure
- Database backed up
- Secrets stored securely
- SSL/TLS certificates valid
- DNS records configured
- Load balancer configured (if applicable)
- CDN configured (if applicable)
- Health check endpoints working
Monitoring & Observability
- Application monitoring configured (Sentry, etc.)
- Log aggregation set up
- Metrics collection enabled
- Alerting rules configured
- Uptime monitoring active
Security
- All secrets rotated
- CORS configured properly
- Rate limiting enabled
- Security headers configured
- Database credentials use least privilege
- OAuth apps configured for production domains
🚀 Deployment Strategies
Strategy 1: Blue-Green Deployment
Concept: Run two identical environments (blue and green), switch traffic between them.
Benefits:
- Zero downtime
- Instant rollback
- Full testing before cutover
Process:
# 1. Deploy to green environment (while blue serves traffic)
docker compose -f docker-compose.green.yml up -d --build
# 2. Run health checks on green
curl https://green.api.yourdomain.com/health
# 3. Run smoke tests
pnpm test:smoke --host=green.api.yourdomain.com
# 4. Switch load balancer from blue to green
# (This is platform-specific)
# 5. Monitor for issues
docker compose -f docker-compose.green.yml logs -f
# 6. If all good, blue becomes the next staging environment
Rollback:
# Switch load balancer back to blue
# Takes effect immediately
Strategy 2: Rolling Deployment
Concept: Update instances one at a time, maintaining availability.
Benefits:
- Gradual rollout
- Detect issues early
- Resource-efficient
Process:
# Using Docker Swarm
docker service update \
--image izri-api:v2.0.0 \
--update-parallelism 1 \
--update-delay 30s \
izri-api
# Or manually with docker compose
for i in {1..3}; do
docker compose up -d --no-deps --build api-$i
sleep 30
# Check health
curl http://api-$i:4000/health
done
Rollback:
# Roll back to previous image
docker service update \
--rollback \
izri-api
Strategy 3: Canary Deployment
Concept: Route small percentage of traffic to new version.
Benefits:
- Minimal risk
- Real user feedback
- Gradual rollout
Process:
# 1. Deploy canary version
docker compose -f docker-compose.canary.yml up -d
# 2. Configure load balancer: 95% to stable, 5% to canary
# (Platform-specific configuration)
# 3. Monitor metrics
# Compare error rates, latency, etc.
# 4. If metrics good, increase canary traffic
# 95% → 90% → 75% → 50% → 0% (all canary)
# 5. Promote canary to stable
Rollback:
# Route all traffic back to stable
# Remove canary containers
docker compose -f docker-compose.canary.yml down
🛠️ Platform-Specific Deployments
Option 1: Docker Compose (VPS)
Setup (DigitalOcean, AWS EC2, etc.):
# 1. SSH into server
ssh user@your-server.com
# 2. Clone repository
git clone https://github.com/yourorg/izri.git
cd izri
# 3. Create .env file
nano .env
# Add production environment variables
# 4. Build images
docker compose build
# 5. Start services
docker compose up -d
# 6. Run migrations
docker compose exec api pnpm db:migrate
# 7. Check health
curl http://localhost:4000/health
Update deployment:
# 1. Pull latest code
git pull origin main
# 2. Rebuild changed services
docker compose up -d --build
# 3. Check logs
docker compose logs -f
Option 2: Railway
Izri's actual production target. See railway.md for the per-service railway.toml files, env vars, the tag-triggered GitHub Actions workflow, and rollback procedures.
Option 3: Render
Configuration (render.yaml):
services:
# API Service
- type: web
name: izri-api
env: docker
dockerfilePath: ./docker/api/Dockerfile
dockerContext: .
envVars:
- key: DATABASE_URL
fromDatabase:
name: postgres
property: connectionString
- key: REDIS_URL
fromService:
name: redis
type: redis
property: connectionString
- key: NODE_ENV
value: production
- key: GITHUB_CLIENT_SECRET
sync: false # Set manually
- key: OPENAI_API_KEY
sync: false
healthCheckPath: /health
# Web Service
- type: web
name: izri-web
env: docker
dockerfilePath: ./docker/web/Dockerfile
dockerContext: .
envVars:
- key: VITE_API_URL
value: https://izri-api.onrender.com
databases:
- name: postgres
databaseName: izri
user: izri
- name: redis
type: redis
Deploy:
# Push to GitHub
git push origin main
# Render auto-deploys on push (if configured)
# Or manually trigger via dashboard
Option 4: Fly.io
Setup:
# 1. Install Fly CLI
curl -L https://fly.io/install.sh | sh
# 2. Login
fly auth login
# 3. Launch app
fly launch
Configuration (fly.toml):
app = "izri-api"
primary_region = "iad"
[build]
dockerfile = "docker/api/Dockerfile"
[env]
NODE_ENV = "production"
PORT = "4000"
[[services]]
internal_port = 4000
protocol = "tcp"
[[services.ports]]
port = 80
handlers = ["http"]
[[services.ports]]
port = 443
handlers = ["tls", "http"]
[[services.http_checks]]
interval = "30s"
timeout = "2s"
method = "get"
path = "/health"
[deploy]
strategy = "rolling"
Deploy:
# Deploy
fly deploy
# Set secrets
fly secrets set \
DATABASE_URL="postgresql://..." \
GITHUB_CLIENT_SECRET="..." \
OPENAI_API_KEY="sk-..."
# View logs
fly logs
# SSH into instance
fly ssh console
Option 5: Kubernetes
Deployment (k8s/deployment.yaml):
apiVersion: apps/v1
kind: Deployment
metadata:
name: izri-api
spec:
replicas: 3
selector:
matchLabels:
app: izri-api
template:
metadata:
labels:
app: izri-api
spec:
containers:
- name: api
image: ghcr.io/yourorg/izri-api:latest
ports:
- containerPort: 4000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: izri-secrets
key: database-url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 4000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 4000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: izri-api
spec:
selector:
app: izri-api
ports:
- port: 80
targetPort: 4000
type: LoadBalancer
Deploy:
# Apply configuration
kubectl apply -f k8s/
# Check status
kubectl get pods
kubectl get services
# View logs
kubectl logs -f deployment/izri-api
# Scale
kubectl scale deployment izri-api --replicas=5
📊 Database Migrations
Pre-Migration Checks
# 1. Backup database
docker compose exec postgres pg_dump -U postgres izri > backup.sql
# Or for managed databases
pg_dump $DATABASE_URL > backup-$(date +%Y%m%d-%H%M%S).sql
# 2. Verify backup
psql $DATABASE_URL < backup.sql --dry-run
Running Migrations
Development/staging first:
# Test in staging
pnpm db:migrate
# Verify application works
pnpm test:integration
Production migration:
# Option 1: Via API container
docker compose exec api pnpm db:migrate
# Option 2: Standalone script
docker run --rm \
-e DATABASE_URL=$DATABASE_URL \
izri-api \
pnpm db:migrate
# Option 3: Platform-specific
railway run pnpm db:migrate
fly ssh console -C "pnpm db:migrate"
Rolling Back Migrations
# If using Drizzle with down migrations
pnpm db:rollback
# Or restore from backup
psql $DATABASE_URL < backup.sql
🔄 Rollback Procedures
Quick Rollback (Docker)
# 1. Identify previous working image
docker images izri-api
# 2. Tag and deploy
docker tag izri-api:v1.2.3 izri-api:latest
docker compose up -d --no-deps api
# 3. Monitor
docker compose logs -f api
Git-Based Rollback
# 1. Find last working commit
git log --oneline
# 2. Revert to that commit
git revert <commit-hash>
# 3. Deploy
git push origin main
# Triggers CI/CD deployment
# Or manual
docker compose up -d --build
Database Rollback
# 1. Stop application
docker compose stop api web
# 2. Restore database
psql $DATABASE_URL < backup-before-migration.sql
# 3. Deploy previous app version
docker compose up -d
# 4. Verify
curl http://localhost:4000/health
🏥 Health Checks & Monitoring
Health Check Endpoint
Implementation (src/routes/health.ts):
import { Hono } from 'hono'
import { db } from '@izri/database'
import { redis } from '../lib/redis'
const health = new Hono()
health.get('/health', async (c) => {
const checks = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
database: 'unknown',
redis: 'unknown',
}
try {
// Check database
await db.execute('SELECT 1')
checks.database = 'ok'
} catch (error) {
checks.database = 'error'
checks.status = 'degraded'
}
try {
// Check Redis
await redis.ping()
checks.redis = 'ok'
} catch (error) {
checks.redis = 'error'
checks.status = 'degraded'
}
const statusCode = checks.status === 'ok' ? 200 : 503
return c.json(checks, statusCode)
})
export { health }
Readiness vs Liveness
Readiness: Can the service handle requests?
health.get('/ready', async (c) => {
// More thorough checks
const ready =
await checkDatabase() &&
await checkRedis() &&
await checkExternalAPIs()
return c.json({ ready }, ready ? 200 : 503)
})
Liveness: Is the service running?
health.get('/live', async (c) => {
// Quick check - just respond
return c.json({ alive: true })
})
📈 Performance Optimization
1. Build Optimization
# Use buildkit for faster builds
# DOCKER_BUILDKIT=1 docker build .
# Multi-stage builds (already implemented)
# Cache dependencies separately
# Only copy what's needed to runtime
2. Resource Limits
# docker-compose.yml
services:
api:
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
3. Connection Pooling
// Database connection pool
import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg'
const pool = new pg.Pool({
connectionString: env.DATABASE_URL,
max: 20, // Maximum connections
idleTimeoutMillis: 30000, // Close idle connections
connectionTimeoutMillis: 2000,
})
export const db = drizzle(pool)
4. Caching Strategy
import { Redis } from 'ioredis'
const redis = new Redis(env.REDIS_URL)
// Cache database queries
async function getProject(id: string) {
const cacheKey = `project:${id}`
// Check cache
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
// Query database
const project = await db.query.projects.findFirst({
where: eq(projects.id, id)
})
// Store in cache
await redis.setex(cacheKey, 300, JSON.stringify(project))
return project
}
🔗 Related Documentation
- Docker: Docker configuration
- Docker Compose: Multi-service setup
- Environment: Environment variables
- Monitoring: Monitoring and logging
Need monitoring setup? Continue with Monitoring & Logging →