Blue-Green Deploys for a Ruby App

Khimananda Oli 8 min read Programming and Languages
Blue-Green Deploys for a Ruby App

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.

Nginx LBBLUE (Active)Puma v3.4.1GREEN (Idle)Puma v3.5.0PostgreSQLShared PrimaryLIVESTANDBY
Traffic flows exclusively to the active Blue environment while Green remains provisioned but isolated until promotion.

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.

Phase 1: ExpandBlue (v1.0)Green (v1.1)Phase 3: ContractAdd column 'email_verified' (nullable)No code changes yet • Safe for both versionsContinues reading/writing legacy columns onlyUnaffected by schema expansionDual-write: populates 'email_verified' + legacyValidated via /healthz before promotionDrop legacy columns after green is stableSeparate deploy • Rollback-safeShared DBSingle Primary✓ Backward compat✓ No sync lag✓ Instant rollback✗ Destructive ops✗ Lock-heavy DDL
Backward-compatible migration phases ensure both blue and green Ruby environments coexist safely against a shared database.

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.

  1. Deploy code and run migrations (expand phase) on green servers.
  2. Start Puma workers and wait for socket/bind confirmation.
  3. Poll http://10.0.2.10:3000/healthz until it returns 200 three consecutive times.
  4. Run synthetic transactions (create test record, read it back, delete it).
  5. 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.

CriteriaBlue-Green DeployRolling Update
DowntimeZero (atomic switch)Near-zero (connection drain window)
Rollback SpeedInstant (revert upstream)Slow (redeploy previous version)
Infrastructure Cost2× during deploy windowMinimal overhead
Database CompatibilityRequires backward-compat migrationsSame requirement
Testing ConfidenceFull production replica validationPartial subset testing
ComplexityModerate (Nginx + orchestration)Low (native orchestrator support)
Best ForCompliance, high-traffic, financial appsInternal 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.

BLUE-GREENRollback: < 5 secondsRevert Nginx symlink + reloadCost: 2× During WindowParallel infra for ~15-30 minValidation: Full ReplicaSmoke test entire green stackRisk: Migration ComplexityROLLING UPDATERollback: 5-15 MinutesRedeploy previous artifactCost: Minimal OverheadReplace instances incrementallyValidation: Partial SubsetOnly updated pods tested liveRisk: Mixed Versions Briefly
Blue-green prioritizes rollback speed and validation confidence at higher temporary cost; rolling optimizes for resource efficiency.

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_timeout in 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.

Frequently Asked Questions

It runs two identical production environments. Traffic switches instantly from the old blue stack to the new green stack after validation, enabling zero-downtime releases and immediate rollbacks for Ruby on Rails applications without affecting active users.

Update your load balancer or reverse proxy upstream configuration to point to the green environment's health-checked endpoints. Tools like NGINX Plus or AWS ALB allow atomic weight changes, ensuring all new requests route to green while draining blue connections gracefully.

Yes. Capistrano supports this via custom tasks that provision parallel directories and symlink the current release. However, true infrastructure-level blue-green usually requires container orchestration or separate EC2 instances rather than just filesystem symlinks on a single server.

Migrations must be backward compatible. Deploy schema changes first that support both old and new code versions. Run non-destructive migrations before switching traffic, then execute cleanup migrations only after confirming the green deployment is stable and fully serving production load.

Temporarily, yes. You pay for double compute resources during the transition window. Most teams minimize this by keeping the blue environment active only until validation passes, typically thirty minutes to an hour, before terminating old instances to restore baseline spending.

Rolling updates replace instances gradually, risking mixed-version states. Blue-green maintains complete environment isolation, allowing instant full-traffic switching and safer rollbacks. Blue-green avoids partial failures but requires double capacity, whereas rolling saves resources but complicates debugging version conflicts.

Run automated smoke tests against the green endpoint using its internal IP or preview URL. Verify critical paths like authentication, payment processing, and API responses. Check application logs and metrics dashboards for error rate spikes before updating the load balancer to serve public traffic.

Only if you manage job queues carefully. Pause workers on blue before switching, ensure pending jobs complete or migrate safely, then start green workers. Use unique queue names per environment to prevent green workers from processing stale blue jobs during transition.

Store sessions externally in Redis or Memcached, not in local memory or cookies tied to specific servers. This ensures users maintain authentication state regardless of which environment serves their request. Verify session store connectivity in green before routing live traffic.

Revert the load balancer configuration to point back to blue immediately. Since blue remains untouched and running during validation, rollback takes seconds. Investigate green logs separately without impacting users, fix the issue, and redeploy to green for another attempt.

No. Both environments typically share the same production database to avoid data synchronization complexity. Rely on backward-compatible migrations instead. Separate databases introduce replication lag risks and make rollbacks significantly harder since data written to green won't exist in blue.

Retain blue for at least one business cycle or monitoring window, typically one to four hours. This allows catching delayed issues like cached responses or batch job failures. Extend retention if your app has low-traffic periods where bugs might remain hidden.

Yes. Use two ReplicaSets with distinct labels and a Service selector swap. Alternatively, use Argo Rollouts or Flagger for automated canary-to-blue-green promotion. Kubernetes handles pod readiness probes and graceful termination, reducing manual scripting compared to bare-metal or VM-based approaches.

Skipping backward-compatible migrations, sharing mutable file storage between environments, forgetting externalized sessions, and inadequate smoke testing. Also avoid hardcoding environment-specific URLs in application config. Always test the rollback procedure itself, not just the forward deployment path.

Often not. The operational overhead and doubled infrastructure cost outweigh benefits for low-traffic apps. Consider simpler strategies like rolling deploys or maintenance windows until your user base or compliance requirements justify zero-downtime guarantees and instant rollback capabilities.