Talenttrust Backend uses a blue/green deployment strategy to enable zero-downtime updates with automatic rollback on health check failure. This runbook covers the operator procedures for managing deployments.
┌─────────────────────────────────────────────────────────────┐
│ Load Balancer / DNS │
│ (port 3000) │
└──────────────────┬──────────────────────────────────────────┘
│
┌──────────┴──────────┐
│ │
┌────▼─────┐ ┌────▼─────┐
│ Blue │ │ Green │
│ (3001) │ │ (3002) │
│ ACTIVE │ │ STANDBY │
└──────────┘ └──────────┘
Router Health Check (3000): Directs traffic to active instance
Blue (3001): Currently serving production traffic
Green (3002): Staged with new code, ready for promotion
npm run deploy:statusOutput:
{
"status": "success",
"data": {
"activeColor": "blue",
"lastSwitch": 1685875200000,
"blueHealth": { "healthy": true, "lastCheck": "2024-06-05T10:00:00Z" },
"greenHealth": { "healthy": false, "lastCheck": "2024-06-05T10:05:00Z" }
}
}npm run deploy:switch-greenBehavior:
- Polls green instance's health endpoint (
GET /health/ready) - Waits up to 30s for green to become healthy
- Updates router to direct traffic to green (3002)
- Green becomes active; blue becomes standby
- Returns 202 Accepted if switch initiated, 200 OK if already green
Output on success:
{
"status": "success",
"message": "Switched to green successfully."
}Output if green unhealthy:
{
"error": {
"code": "bad_gateway",
"message": "Green instance is not healthy. Switch aborted."
}
}npm run deploy:rollbackBehavior:
- Switches router back to blue (3001)
- Blue becomes active; green becomes standby
- No-op if already on blue
Output:
{
"status": "success",
"message": "Rolled back to blue successfully.",
"data": { "activeColor": "blue" }
}Before initiating a deployment, verify:
✅ Green instance is ready
curl -s http://localhost:3002/health/ready | jq .Expected: { "healthy": true }
✅ Blue instance is healthy (current production)
curl -s http://localhost:3001/health/ready | jq .Expected: { "healthy": true }
✅ Router is accessible
curl -s http://localhost:3000/health | jq .Expected: { "status": "ok" }
✅ Environment variables are set
echo $JWT_SECRET
echo $DATABASE_URLAll required secrets must be present.
# Start green instance with new code
NODE_ENV=production PORT=3002 npm start &
# Wait for green to become healthy (max 30s)
for i in {1..30}; do
if curl -s http://localhost:3002/health/ready | grep -q '"healthy":true'; then
echo "Green is ready after $i seconds"
break
fi
sleep 1
donenpm run deploy:switch-greenMonitor the switch:
# Watch router logs
tail -f logs/router.log | grep "switch\|promotion"
# Verify new traffic goes to green
curl -s http://localhost:3000/health | jq .activeColor
# Should return: "green"# Check key endpoints on green
curl -s http://localhost:3002/api/v1/health | jq .
curl -s http://localhost:3002/api/v1/admin/deploy/status | jq .
# Monitor error rates for 2-5 minutes
tail -f logs/application.log | grep ERROR
# Check database connections
npm run db:statusAfter green has been active for 5+ minutes:
# Gracefully shutdown blue
npm run deploy:drain:blue
# Wait for in-flight requests to complete (max 30s)
sleep 30
# Verify blue is offline
curl -s http://localhost:3001/health 2>&1 | grep -q "Connection refused" && echo "Blue shutdown complete"The switch-green command includes an automatic health gate that prevents switching to an unhealthy instance.
Health Gate Flow:
switch-green
│
├─► Poll green:/health/ready
│ (interval: 1s, timeout: 30s)
│
├─► Healthy? ──[YES]──► Update router, return 202
│ │
│ └─► Monitor for 5min
│ Auto-rollback if errors spike
│
└─► Unhealthy? ──[YES]──► Abort switch, return 502
(Green not ready)
Health Check Details:
- Endpoint:
GET /health/ready - Expected response:
{ "healthy": true } - Timeout: 1 second per request
- Poll interval: 1 second
- Total timeout: 30 seconds
Health criteria:
- Database connectivity OK
- Required services responding
- No critical startup errors
- Memory usage within limits
If enabled, the deployment monitors error rates and automatically rolls back if:
- Error rate spike: >50% increase over baseline (5-minute window)
- Database connection failures: >5 consecutive errors
- Critical service unavailable: Health endpoint unreachable for >10s
- Memory exhaustion: Heap usage >90%
To enable auto-rollback:
export DEPLOY_AUTO_ROLLBACK=true
export DEPLOY_ERROR_THRESHOLD=50 # percent increase
export DEPLOY_MONITOR_DURATION=300000 # 5 minutes in ms
npm run deploy:switch-greenAuto-rollback event:
# Logs will show
[WARN] Deployment monitoring detected error spike (78% > 50%)
[WARN] Initiating automatic rollback...
[INFO] Rolled back to blue successfullynpm run deploy:statusResponse:
{
"status": "success",
"data": {
"activeColor": "blue",
"lastSwitch": 1685875200000,
"blueHealth": {
"healthy": true,
"lastCheck": "2024-06-05T10:00:00Z",
"endpoint": "http://localhost:3001/health/ready"
},
"greenHealth": {
"healthy": false,
"lastCheck": "2024-06-05T10:05:00Z",
"endpoint": "http://localhost:3002/health/ready",
"error": "ECONNREFUSED"
},
"switchInProgress": false
}
}Field Meanings:
| Field | Meaning |
|---|---|
activeColor |
Current active instance: blue or green |
lastSwitch |
Timestamp of last successful switch (epoch ms) |
blueHealth.healthy |
Is blue instance healthy? |
greenHealth.healthy |
Is green instance healthy? |
switchInProgress |
Is a switch operation currently running? |
.error |
Error message if health check failed |
Interpretation Guide:
Healthy deployment:
activeColor: "blue"
blueHealth.healthy: true
greenHealth.healthy: true
switchInProgress: false
→ Safe to perform a switch
Ready for rollback:
activeColor: "green"
blueHealth.healthy: true
greenHealth.healthy: true
switchInProgress: false
→ Run npm run deploy:rollback to return to blue
Degraded state:
activeColor: "blue"
blueHealth.healthy: true
greenHealth.healthy: false
→ Green needs attention; do NOT attempt switch
In-flight switch:
switchInProgress: true
→ Wait for completion; do NOT issue new commands
npm run deploy:drain:blueWhat happens:
- Blue stops accepting new connections
- In-flight requests have up to 30 seconds to complete
- Long-polling clients are notified to reconnect to green
- Database connections are closed gracefully
- Blue process exits
Drain timeout:
- Hard timeout: 30 seconds
- Graceful period: 25 seconds
- Force kill: 5 seconds
npm run deploy:kill:blue # Force kill blue immediatelynpm run deploy:rollbackUse when:
- New code has a critical bug
- Error rate is unacceptable
- Data corruption is detected
- Operator decision
Time to complete: 2-5 seconds
If auto-rollback is enabled and error thresholds are exceeded:
- Automatic switch back to blue
- Alert sent to operations team
- Post-mortem recommended
To check if auto-rollback was triggered:
grep "auto.rollback" logs/application.logSymptoms:
npm run deploy:switch-green
# Returns: "Green instance is not healthy. Switch aborted."
Diagnosis:
# Check green logs
tail -100 logs/green.log | grep ERROR
# Check green health endpoint directly
curl -v http://localhost:3002/health/ready
# Check database connectivity
curl http://localhost:3002/api/v1/health | jq .database
# Check environment variables on green
ssh green-instance-ip
echo $DATABASE_URL
echo $JWT_SECRETResolution:
- Fix the issue on green instance
- Restart green:
npm run deploy:restart:green - Wait 30 seconds for health checks
- Retry:
npm run deploy:switch-green
Symptoms:
npm run deploy:switch-green
# No response after 30 seconds
Diagnosis:
# Check if green is responding
curl -w "\n%{http_code}\n" http://localhost:3002/health/ready
# Check if port 3002 is open
netstat -tuln | grep 3002
# Check process
ps aux | grep "PORT=3002"Resolution:
- Kill the switch command:
Ctrl+C - Verify green is still running
- Check for network issues
- Try again:
npm run deploy:switch-green
Symptoms:
curl http://localhost:3001/api/v1/health 200 OK
curl http://localhost:3002/api/v1/health 200 OK
curl http://localhost:3000/api/v1/health 502 Bad Gateway
Diagnosis:
# Check router process
ps aux | grep "PORT=3000"
# Check router logs
tail -50 logs/router.log
# Verify active color
npm run deploy:status | jq .activeColor
# Test direct connection to active instance
ACTIVE=$(npm run deploy:status | jq -r .activeColor)
curl http://localhost:300${ACTIVE:0:1}/healthResolution:
- Restart router:
npm run deploy:restart:router - Verify connection to active instance
- Check firewall rules
During deployment:
- Error rate (should stay <1%)
- p95 latency (should stay <500ms)
- Database connection pool usage
- Memory usage on both instances
- Request volume per instance
alerts:
- name: deployment_switch_failed
condition: switch_failure > 0
severity: critical
- name: auto_rollback_triggered
condition: auto_rollback_event == true
severity: critical
- name: health_gate_timeout
condition: health_check_timeout > 30s
severity: high
- name: error_rate_spike
condition: error_rate > baseline * 1.5
severity: highProduction error detected
│
├─► Severity: CRITICAL (data loss, security)
│ └─► IMMEDIATE ROLLBACK
│ npm run deploy:rollback
│
├─► Severity: HIGH (service down, major bug)
│ └─► Check auto-rollback status
│ if not auto-rolled back:
│ npm run deploy:rollback
│
├─► Severity: MEDIUM (degraded performance)
│ └─► Monitor for 5 minutes
│ if worsens:
│ npm run deploy:rollback
│ else:
│ investigate and fix forward
│
└─► Severity: LOW (non-user impacting)
└─► Investigate without rollback
Fix in next deployment
# Deployment Configuration
DEPLOY_AUTO_ROLLBACK=true # Enable automatic rollback
DEPLOY_ERROR_THRESHOLD=50 # Error rate increase threshold (%)
DEPLOY_MONITOR_DURATION=300000 # Monitoring window (ms)
DEPLOY_HEALTH_TIMEOUT=30000 # Health gate timeout (ms)
DEPLOY_HEALTH_POLL_INTERVAL=1000 # Poll interval (ms)
# Instance Configuration
BLUE_PORT=3001 # Blue instance port
GREEN_PORT=3002 # Green instance port
ROUTER_PORT=3000 # Router portnpm run deploy:status # Check current deployment state
npm run deploy:switch-green # Promote green to active
npm run deploy:rollback # Return to blue (previous)
npm run deploy:drain:blue # Gracefully shutdown blue
npm run deploy:kill:blue # Force kill blue
npm run deploy:restart:green # Restart green instance
npm run deploy:restart:router # Restart router
npm run db:status # Check database connectivity- Health Checks: See
docs/health.mdfor readiness and liveness probe behavior - Graceful Shutdown: See
docs/shutdown.mdfor drain timing and connection cleanup - Admin Auth: See
docs/api-keys.mdfor authenticated deploy endpoint access - Monitoring: See
docs/observability.mdfor metrics and alerting - Troubleshooting: See
docs/troubleshooting.mdfor common issues
Deploy endpoints (/api/v1/admin/deploy/*) require admin authentication:
- JWT:
Authorization: Bearer <admin-token> - API Key:
X-API-Key: <admin-api-key>withdeploy:*scope
- Blue/green ports are environment-based
- Database URLs and secrets in
.envonly - Router configuration injected at startup
All deploy operations are logged:
[AUDIT] actor=admin-user@example.com action=deploy:switch-green resource=deploy.blue->green
[AUDIT] actor=admin-user@example.com action=deploy:rollback resource=deploy.green->blue
#!/bin/bash
# Complete safe deployment workflow
set -e
echo "=== Pre-Deployment Checks ==="
curl -s http://localhost:3001/health/ready | jq .
curl -s http://localhost:3002/health/ready | jq .
echo "=== Current Status ==="
npm run deploy:status
echo "=== Starting Green with New Code ==="
export NODE_ENV=production
export PORT=3002
npm start &
GREEN_PID=$!
echo "=== Waiting for Green to be Ready ==="
sleep 5
echo "=== Switching to Green ==="
npm run deploy:switch-green
echo "=== Validating Green (5 minute window) ==="
sleep 30
tail logs/application.log | grep -c ERROR || echo "No errors detected"
echo "=== Draining Blue ==="
npm run deploy:drain:blue
echo "=== Deployment Complete ==="
npm run deploy:status | jq .
echo "✓ Deployment successful"The promoter module (src/deployment/promoter.ts) orchestrates environment
promotions (dev → staging → production) with validation, blue-green switching,
audit logging, and persisted history.
PromotionRequest
│
▼
┌─────────────────────────────┐
│ validatePromotionPath │ ← dev→staging or staging→production only
│ └─ valid? ─── NO ──► 400 │
└───────────┬─────────────────┘
│ YES
▼
┌─────────────────────────────┐
│ loadEnvironmentConfig │ ← transient NODE_ENV swap for target
└───────────┬─────────────────┘
│
▼
┌─────────────────────────────┐
│ validateDeploymentReadiness │ ← port/URL/CORS/Stellar validation
│ └─ valid? ─── NO ──► 400 │
└───────────┬─────────────────┘
│ YES
▼
┌─────────────────────────────┐
│ performHealthCheck │ ← GET /health/ready (non-fatal)
└───────────┬─────────────────┘
│
▼
┌─────────────────────────────┐
│ switchToGreen (deploy.ts) │ ← blue-green state machine
│ └─ throws? ─── YES ──► │
│ record FAILURE + audit│
└───────────┬─────────────────┘
│ OK
▼
┌─────────────────────────────┐
│ recordPromotion (SUCCESS) │ ← INSERT into deployment_history
│ auditService.log (INFO) │
└─────────────────────────────┘
RollbackRequest
│
▼
┌─────────────────────────────┐
│ targetVersion present? │ ← required
│ environment ≠ development? │
│ └─ NO ──► 400 │
└───────────┬─────────────────┘
│ YES
▼
┌─────────────────────────────┐
│ rollback (deploy.ts) │ ← reverts to previous colour
│ └─ throws? ─── YES ──► │
│ record FAILURE + audit│
└───────────┬─────────────────┘
│ OK
▼
┌─────────────────────────────┐
│ recordRollback (SUCCESS) │ ← INSERT into deployment_history
│ auditService.log (WARNING) │
└─────────────────────────────┘
getPromotionHistory(environment) queries deployment_history for all rows
where the environment appears as source or target, ordered by timestamp
descending. Each record includes:
| Field | Source |
|---|---|
from |
environment_from |
to |
environment_to |
version |
target_version |
initiatedBy |
initiated_by |
timestamp |
ISO-8601 in UTC |
| Event | Severity | When |
|---|---|---|
DEPLOYMENT_PROMOTED |
INFO |
Successful promotion |
DEPLOYMENT_PROMOTED |
CRITICAL |
Failed promotion |
DEPLOYMENT_ROLLED_BACK |
WARNING |
Successful rollback |
DEPLOYMENT_ROLLED_BACK |
CRITICAL |
Failed rollback |
| From | To |
|---|---|
development |
staging |
staging |
production |
production |
(none) |
test |
(none) |
Any other path produces a ValidationResult with valid: false and a
descriptive error message. Direct development → production promotions emit
a warning but are currently rejected by the path validator.
Last Updated: 2026-07-25
Version: 2.0
Audience: Operators, DevOps, On-call Engineers