
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Downtime during releases is an operational failure, not an inevitability. Implementing blue-green deploys for a Ruby app eliminates maintenance windows by running two identical production environments and switching traffic instantly at the load balancer layer. This guide covers the practical Nginx and Puma configuration required to make this work reliably.
How do you configure Nginx upstreams for blue-green deploys for a Ruby app?
The core mechanism behind safe blue-green and canary deployments strategies compared is atomic traffic switching. For Ruby applications running on Puma or Unicorn, Nginx serves as the definitive gatekeeper. You must define separate upstream blocks rather than relying on a single backend pool. This separation ensures that connection draining happens naturally when you stop sending new requests to the old version.
Define parallel upstream blocks
Your Nginx configuration should explicitly name each environment. Avoid generic names like backend; use version-aware or color-aware labels to prevent operator confusion during incidents. Place these in a dedicated config file included by your main server block.
# /etc/nginx/conf.d/ruby_upstreams.conf
upstream ruby_blue {
least_conn;
server 10.0.1.10:3000 max_fails=3 fail_timeout=30s;
server 10.0.1.11:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
upstream ruby_green {
least_conn;
server 10.0.2.10:3000 max_fails=3 fail_timeout=30s;
server 10.0.2.11:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
} Implement the atomic switch
Create a symlink or a variable-based include to control which upstream receives traffic. The most reliable method in production is updating a symlink and reloading Nginx. This avoids parsing errors that can occur with complex variable logic in high-throughput scenarios.
# /etc/nginx/sites-available/ruby-app.conf
server {
listen 443 ssl http2;
server_name app.example.com;
# Active upstream controlled by symlink: /etc/nginx/active_ruby_upstream
include /etc/nginx/active_ruby_upstream;
location / {
proxy_pass http://$active_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# Dedicated health endpoint for validation
location /healthz {
proxy_pass http://$active_backend/healthz;
access_log off;
}
} When deploying, update the /etc/nginx/active_ruby_upstream file to point to the new upstream block, then run nginx -t && systemctl reload nginx. The reload is graceful: existing connections finish on the old upstream while new requests flow to the green environment.
What database migration strategy works with blue-green Ruby deployments?
The hardest part of blue-green deploys for a Ruby app isn't the web server—it's the database. Both environments typically share the same PostgreSQL or MySQL primary to avoid data synchronization lag. This means your migrations must be backward-compatible. A destructive schema change will break whichever environment is still running the old code.
I follow a strict three-phase migration protocol for any client running SOC 2 compliant infrastructure:
- Phase 1 (Expand): Add new columns or tables without removing old ones. Deploy this migration before either blue or green updates. Both old and new code must function correctly with the expanded schema.
- Phase 2 (Migrate Code): Deploy the new Ruby application to the green environment. The new code writes to both old and new columns (dual-write) or reads from the new structure while tolerating nulls. Validate thoroughly.
- Phase 3 (Contract): After the green environment is promoted and stable, deploy a subsequent release that removes deprecated columns and stops dual-writing. This cleanup happens in a future deployment cycle, never during the initial cutover.
For teams managing PostgreSQL administration essentials, use CONCURRENTLY options for index creation and avoid locking operations during peak hours. If your schema changes cannot be made backward-compatible, consider using logical replication to maintain separate databases, though this adds significant operational complexity and latency.
How do you validate a green environment before switching traffic?
Never switch traffic based solely on deployment script success. Automated smoke tests and synthetic health checks are mandatory. In my experience auditing release processes, teams that skip this step account for nearly all post-deployment incidents involving blue-green deploys for a Ruby app.
Implement a comprehensive health endpoint
Your Rails or Sinatra application must expose a /healthz endpoint that verifies critical dependencies, not just process liveness. A common mistake is returning 200 OK when the database connection pool is exhausted or Redis is unreachable.
# app/controllers/health_controller.rb
class HealthController < ApplicationController
skip_before_action :authenticate_user!
def show
checks = {
database: ActiveRecord::Base.connection.active?,
redis: Redis.current.ping == "PONG",
version: ENV["APP_VERSION"]
}
if checks.values.all?
render json: { status: "healthy", checks: checks }, status: :ok
else
render json: { status: "unhealthy", checks: checks }, status: :service_unavailable
end
rescue StandardError => e
render json: { status: "error", message: e.message }, status: :internal_server_error
end
end Run pre-switch validation scripts
Before updating the Nginx upstream, execute a validation script that hits the green environment directly via its internal IP. This bypasses the load balancer and confirms the new stack is functional.
- Deploy code and run migrations (expand phase) on green servers.
- Start Puma workers and wait for socket/bind confirmation.
- Poll
http://10.0.2.10:3000/healthzuntil it returns 200 three consecutive times. - Run synthetic transactions (create test record, read it back, delete it).
- Only then execute the Nginx upstream switch.
If validation fails, the deployment halts. The blue environment continues serving traffic unaffected. This is the safety guarantee that makes the pattern worthwhile. For deeper observability integration, refer to the four golden signals of monitoring to ensure your health checks align with actual user experience metrics.
What are the trade-offs between blue-green and rolling deploys for Ruby?
Choosing between deployment strategies depends on your team's risk tolerance, infrastructure budget, and compliance requirements. While rolling updates are cheaper, blue-green deploys for a Ruby app provide deterministic rollback and isolation that rolling cannot match. Understanding these trade-offs prevents over-engineering simple apps or under-protecting critical ones.
| Criteria | Blue-Green Deploy | Rolling Update |
|---|---|---|
| Downtime | Zero (atomic switch) | Near-zero (connection drain window) |
| Rollback Speed | Instant (revert upstream) | Slow (redeploy previous version) |
| Infrastructure Cost | 2× during deploy window | Minimal overhead |
| Database Compatibility | Requires backward-compat migrations | Same requirement |
| Testing Confidence | Full production replica validation | Partial subset testing |
| Complexity | Moderate (Nginx + orchestration) | Low (native orchestrator support) |
| Best For | Compliance, high-traffic, financial apps | Internal tools, low-risk services |
In regulated environments or high-traffic Nepali e-commerce platforms where a failed release during peak hours could mean significant revenue loss, the 2× temporary cost is justified by the instant rollback capability. For internal dashboards or development APIs, rolling updates through Kubernetes or Capistrano are often sufficient. If you're operating on Kubernetes specifically, the mechanics differ slightly; see blue-green and canary deploys on Kubernetes for service-mesh and ingress-controller approaches.
How do you handle long-lived connections and WebSocket traffic?
Ruby applications using ActionCable or AnyCable introduce a complication: persistent connections don't respect upstream switches. When you flip Nginx to green, existing WebSocket clients remain connected to blue until they disconnect or timeout. This creates a split-brain scenario where some users interact with old code and others with new.
To manage this gracefully during blue-green deploys for a Ruby app:
- Set explicit timeouts: Configure
proxy_read_timeoutin Nginx to a reasonable value (e.g., 300s) rather than infinite. This forces periodic reconnections. - Broadcast a reconnect signal: Before switching, publish a message through your pub/sub layer instructing all clients to reconnect within a randomized jitter window. This prevents thundering herd on the new green environment.
- Drain before decommission: After switching, keep blue Puma workers alive for at least the maximum connection duration. Only terminate blue after confirming zero active connections via
pumactl stats. - Version-aware messaging: Include API version in WebSocket frames so clients can detect incompatibility and force-refresh automatically.
This adds complexity. If your application relies heavily on real-time features, evaluate whether the operational overhead is justified versus a well-configured rolling update with connection draining. For most request-response Ruby APIs, however, the atomic switch remains the gold standard.
Operationalizing Safe Ruby Releases
Blue-green deploys for a Ruby app transform releases from anxious events into routine operations. The pattern demands discipline around backward-compatible migrations, rigorous health checking, and disciplined Nginx configuration—but the payoff is predictable, reversible software delivery. Start by implementing the dual-upstream Nginx pattern and comprehensive health endpoints described above. Measure your rollback time before and after adoption; the improvement speaks for itself. If your team needs help designing a release strategy that matches your compliance requirements and traffic profile, reach out to discuss your deployment architecture.