• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Zero-Downtime Blue-Green Deployment Pipelines for Ruby Applications on Linode

Zero-Downtime Blue-Green Deployment Pipelines for Ruby Applications on Linode

Understanding the Blue-Green Deployment Pattern

The Blue-Green deployment strategy is a cornerstone of achieving zero-downtime releases. It involves maintaining two identical production environments, “Blue” and “Green.” At any given time, one environment (e.g., Blue) is live and serving all production traffic, while the other (Green) is idle. To deploy a new version, we deploy the updated code to the idle environment (Green), test it thoroughly, and then switch the router to direct all incoming traffic to the newly updated Green environment. The old Blue environment is then kept as a rollback option or updated to the new version for the next cycle.

Infrastructure Setup on Linode

For this setup, we’ll assume you have two identical Linode instances, each capable of running your Ruby application. Let’s call them app-blue-1 and app-green-1. Both instances will run the same web server (e.g., Nginx) and application server (e.g., Puma). A load balancer or reverse proxy is crucial for directing traffic. For simplicity and cost-effectiveness on Linode, we can use a dedicated Linode instance as our load balancer (lb-1) running Nginx, or leverage Linode’s NodeBalancers if a managed solution is preferred. For this example, we’ll configure Nginx on lb-1.

Nginx Configuration for Traffic Switching

The core of the Blue-Green strategy lies in how we manage traffic routing. We’ll configure Nginx on our load balancer (lb-1) to direct traffic to either the Blue or Green application servers. This is achieved using Nginx’s upstream blocks and a dynamic configuration mechanism, often involving symlinks or a simple script to update the Nginx configuration.

Initial Nginx Load Balancer Configuration

On lb-1, create an Nginx configuration file, for instance, /etc/nginx/sites-available/app_router. This file will define our upstream groups and the server block that routes traffic.

/etc/nginx/sites-available/app_router (Initial State – Blue is Live)

# Define the upstream for the BLUE environment
upstream blue_app {
    server 192.168.1.101:8080; # IP of app-blue-1, port Puma is listening on
    # Add more blue servers if scaling within the blue environment
    # server 192.168.1.102:8080;
}

# Define the upstream for the GREEN environment (currently idle)
upstream green_app {
    server 192.168.1.103:8080; # IP of app-green-1, port Puma is listening on
    # Add more green servers if scaling within the green environment
    # server 192.168.1.104:8080;
}

server {
    listen 80;
    server_name your_domain.com;

    location / {
        # Initially, direct all traffic to the blue_app upstream
        proxy_pass http://blue_app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Optional: Health check endpoint for each environment
    # location /health_blue {
    #     access_log off;
    #     proxy_pass http://blue_app;
    # }
    # location /health_green {
    #     access_log off;
    #     proxy_pass http://green_app;
    # }
}

After creating this file, enable it and reload Nginx:

sudo ln -s /etc/nginx/sites-available/app_router /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Application Server Configuration (Puma)

On both app-blue-1 and app-green-1, your Ruby application (e.g., Rails) should be configured to run with Puma, listening on a specific port (e.g., 8080) and accessible only by the load balancer. Ensure your firewall rules (e.g., ufw) on these app servers only allow traffic from the load balancer’s IP address on port 8080.

# Example command to start Puma for a Rails app
# Assuming your app is in /var/www/my_ruby_app
cd /var/www/my_ruby_app
bundle exec puma -C config/puma.rb

Example config/puma.rb snippet

# config/puma.rb
workers Integer(ENV.fetch("WEB_CONCURRENCY") { 2 })
threads_count = Integer(ENV.fetch("RAILS_MAX_THREADS") { 5 })
threads threads_count, threads_count

environment ENV.fetch("RAILS_ENV") { "production" }
port ENV.fetch("PORT") { 8080 }
# bind "tcp://0.0.0.0:8080" # Listen on all interfaces, accessible by LB
bind "tcp://127.0.0.1:8080" # If LB is on the same machine, or use specific IP

# For Blue/Green, ensure it's accessible by the LB IP
# bind "tcp://192.168.1.101:8080" # For app-blue-1
# bind "tcp://192.168.1.103:8080" # For app-green-1
# Or, if using a private network and the LB can reach it:
# bind "tcp://0.0.0.0:8080"
# And firewall rules restrict access to the LB IP.

plugin "tmp_restart"

Automating the Deployment Pipeline

A robust deployment pipeline will automate the process of deploying to the idle environment and switching traffic. This can be orchestrated using CI/CD tools like GitLab CI, GitHub Actions, Jenkins, or even a custom Bash script. The key steps involve:

  • Deploying the new code to the idle (Green) servers.
  • Running automated tests (unit, integration, smoke tests) against the Green environment.
  • If tests pass, updating the Nginx configuration to point to the Green environment.
  • Reloading Nginx to apply the changes.
  • Optionally, performing a final round of smoke tests against the live Green environment.

Deployment Script Example (Bash)

Let’s outline a conceptual Bash script that could be part of your CI/CD pipeline. This script would run on the load balancer server (lb-1) or a dedicated deployment server that has SSH access to all application servers.

#!/bin/bash

# --- Configuration ---
CURRENT_LIVE_ENV="blue" # 'blue' or 'green'
IDLE_ENV="green"
BLUE_APP_SERVERS=("192.168.1.101:8080") # Add more if needed
GREEN_APP_SERVERS=("192.168.1.103:8080") # Add more if needed
APP_PATH="/var/www/my_ruby_app"
GIT_BRANCH="main"
LB_NGINX_CONF="/etc/nginx/sites-available/app_router"
LB_NGINX_ENABLED_CONF="/etc/nginx/sites-enabled/app_router"
SSH_USER="deployer" # User with SSH access and sudo privileges

# --- Helper Functions ---
run_remote() {
    local server=$1
    local cmd=$2
    echo "Executing on $server: $cmd"
    ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${SSH_USER}@${server%%:*} "$cmd"
}

deploy_to_env() {
    local env=$1
    local servers=("${!2}") # Dereference array
    local app_version=$3

    echo "Deploying version $app_version to $env environment..."

    for server_ip_port in "${servers[@]}"; do
        local server_ip=$(echo "$server_ip_port" | cut -d: -f1)
        echo "Deploying to $server_ip..."

        # 1. Pull latest code
        run_remote "$server_ip" "cd $APP_PATH && git fetch origin && git reset --hard origin/$GIT_BRANCH && git pull origin $GIT_BRANCH"

        # 2. Install dependencies
        run_remote "$server_ip" "cd $APP_PATH && bundle install --without development test"

        # 3. Run database migrations (if applicable)
        # Be cautious with migrations in Blue-Green. Often done separately or with care.
        # run_remote "$server_ip" "cd $APP_PATH && bundle exec rails db:migrate RAILS_ENV=production"

        # 4. Precompile assets (if applicable)
        run_remote "$server_ip" "cd $APP_PATH && bundle exec rails assets:precompile RAILS_ENV=production"

        # 5. Restart application server (Puma)
        # This requires a process manager like systemd or supervisord
        run_remote "$server_ip" "sudo systemctl restart my_ruby_app_${env}" # Assumes service name convention
    done
    echo "Deployment to $env complete."
}

switch_traffic() {
    local target_env=$1
    echo "Switching traffic to $target_env environment..."

    # Create a temporary Nginx config file
    local temp_nginx_conf=$(mktemp)
    cp "$LB_NGINX_CONF" "$temp_nginx_conf"

    # Modify the upstream proxy_pass directive
    if [ "$target_env" == "green" ]; then
        # If Green is the target, update proxy_pass to http://green_app
        sed -i 's|proxy_pass http://blue_app;|proxy_pass http://green_app;|' "$temp_nginx_conf"
        echo "Traffic will now go to GREEN."
    elif [ "$target_env" == "blue" ]; then
        # If Blue is the target, update proxy_pass to http://blue_app
        sed -i 's|proxy_pass http://green_app;|proxy_pass http://blue_app;|' "$temp_nginx_conf"
        echo "Traffic will now go to BLUE."
    else
        echo "Invalid target environment: $target_env"
        exit 1
    fi

    # Validate the new configuration
    echo "Validating Nginx configuration..."
    sudo nginx -t -c "$temp_nginx_conf"
    if [ $? -ne 0 ]; then
        echo "Nginx configuration test failed. Aborting traffic switch."
        rm "$temp_nginx_conf"
        exit 1
    fi

    # Replace the live configuration and reload Nginx
    echo "Applying new Nginx configuration..."
    sudo mv "$temp_nginx_conf" "$LB_NGINX_CONF"
    sudo systemctl reload nginx

    echo "Traffic switched to $target_env."
}

# --- Main Deployment Logic ---
APP_VERSION=$(git rev-parse HEAD) # Get current commit hash

echo "Starting Blue-Green deployment for branch $GIT_BRANCH (version: $APP_VERSION)"

# Determine which environment is currently idle
if [ "$CURRENT_LIVE_ENV" == "blue" ]; then
    IDLE_ENV="green"
    IDLE_SERVERS=("${GREEN_APP_SERVERS[@]}")
else
    IDLE_ENV="blue"
    IDLE_SERVERS=("${BLUE_APP_SERVERS[@]}")
fi

# 1. Deploy to the idle environment
deploy_to_env "$IDLE_ENV" IDLE_SERVERS "$APP_VERSION"

# 2. Run automated tests against the idle environment (placeholder)
echo "Running automated tests against $IDLE_ENV environment..."
# Add your test execution commands here.
# Example: curl -f http:///health_check || { echo "Tests failed!"; exit 1; }
sleep 5 # Simulate test duration

# 3. Switch traffic
switch_traffic "$IDLE_ENV"

# 4. Optional: Update CURRENT_LIVE_ENV variable or state file
# This script assumes CURRENT_LIVE_ENV is manually updated or managed externally.
# For a fully automated system, you'd update a state file or database.

echo "Blue-Green deployment completed successfully."
exit 0

Managing Application Server Processes

It’s critical to manage your Puma processes reliably. Using a process manager like systemd is highly recommended. Create separate service files for each environment on the application servers.

Example systemd service file for the Green environment

[Unit]
Description=Puma application server (Green)
After=network.target

[Service]
User=your_app_user
Group=your_app_group
WorkingDirectory=/var/www/my_ruby_app
Environment="RAILS_ENV=production"
Environment="PORT=8080"
# Ensure this path points to your Puma executable
ExecStart=/usr/local/bin/bundle exec puma -C config/puma.rb

# Restart Puma if it exits uncleanly
Restart=on-failure

# Define paths for logs
StandardOutput=append:/var/log/puma_green.log
StandardError=append:/var/log/puma_green.err.log

[Install]
WantedBy=multi-user.target

On each application server (app-blue-1, app-green-1), you would create similar service files (e.g., my_ruby_app_blue.service, my_ruby_app_green.service) and enable them:

# On app-blue-1
sudo cp my_ruby_app_blue.service /etc/systemd/system/
sudo systemctl enable my_ruby_app_blue
sudo systemctl start my_ruby_app_blue

# On app-green-1
sudo cp my_ruby_app_green.service /etc/systemd/system/
sudo systemctl enable my_ruby_app_green
sudo systemctl start my_ruby_app_green

Rollback Strategy

The beauty of Blue-Green is the inherent rollback capability. If a deployment to the Green environment introduces critical issues, you simply reverse the traffic switch. The Nginx configuration on the load balancer is updated to point back to the Blue environment, which is still running the previous stable version. This is typically a single command or script execution.

# Example rollback command (assuming the script above is extended)
# This would effectively switch traffic back to the 'blue' upstream
./deploy.sh --rollback

Considerations and Advanced Scenarios

Database Migrations

Database schema changes are the trickiest part of Blue-Green deployments. If a new version requires schema changes that are incompatible with the old version, you cannot simply deploy and switch. A common approach is to use a “forward and backward compatible” migration strategy:

  • Deploy code that adds new columns/tables or non-breaking changes.
  • Run migrations.
  • Switch traffic.
  • Deploy code that uses the new schema elements.
  • (Later) Deploy code that removes old schema elements.
  • (Later) Run migrations to remove old elements.

Alternatively, migrations can be run against the *current* live environment *before* deploying the new code to the idle environment, or run against the idle environment *after* deployment but *before* traffic switch, provided the application code is written to be compatible with both schema versions during the transition.

Zero-Downtime Database Migrations

For complex migrations, consider tools like gh-ost or pt-online-schema-change, which can perform schema changes with minimal locking and downtime, often in conjunction with Blue-Green deployments.

State Management

The script above assumes CURRENT_LIVE_ENV is a hardcoded variable. In a production system, this state needs to be managed reliably. This could be a simple file on the load balancer server, a key-value store (like etcd or Consul), or managed by your CI/CD orchestrator.

Health Checks

Implement comprehensive health check endpoints in your Ruby application. The load balancer (or your deployment script) should poll these endpoints on the newly deployed environment before switching traffic. This ensures the application is not only running but also responsive and healthy.

Scaling

Both Blue and Green environments can be scaled independently. If you need to scale the Blue environment, you’d add more servers to the blue_app upstream in Nginx and ensure your deployment script can target and deploy to multiple servers within an environment.

Linode NodeBalancers

If you opt for Linode’s managed NodeBalancers, the traffic switching mechanism changes. Instead of manipulating Nginx config, you’d use the Linode API to update the NodeBalancer’s backend targets. This can be more robust but requires integrating API calls into your deployment script.

# Example using Linode API (conceptual, requires linode-cli or similar)
# Assuming 'nodebalancer_id' and 'nodebalancer_config_id' are known

# Get current backend targets
linode-cli nodebalancers configs get-backends --nodebalancer-id $nodebalancer_id --config-id $nodebalancer_config_id

# Disable current backends (e.g., Blue)
# linode-cli nodebalancers configs update-backend --nodebalancer-id $nodebalancer_id --config-id $nodebalancer_config_id --backend-id  --enabled false

# Enable new backends (e.g., Green)
# linode-cli nodebalancers configs update-backend --nodebalancer-id $nodebalancer_id --config-id $nodebalancer_config_id --backend-id  --enabled true

This Blue-Green deployment strategy, when implemented with careful attention to infrastructure, automation, and potential complexities like database migrations, provides a powerful mechanism for delivering updates to your Ruby applications on Linode with confidence and zero downtime.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Go Goroutines vs. Node.js Event Loop: Scaling I/O-Bound Microservices Under High Load
  • Elixir Phoenix vs. Go Gin: Concurrency Models and Fault Tolerance Under Peak Request Volume
  • Python Celery vs. Go Channels: Distributed Task Queue Overhead and Memory Reliability
  • Scala Pekko vs. Go Goroutines: Actor Model vs. CSP for Event-Driven Reactive Systems
  • Java Loom Virtual Threads vs. Go Goroutines: Under-the-Hood Scheduler and Thread Overhead Comparison

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (584)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (806)
  • PHP (5)
  • PHP Development (21)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (19)
  • Ruby on Rails (1)
  • Security & Compliance (543)
  • SEO & Growth (491)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (7)
  • WordPress Theme Development (357)

Recent Posts

  • Go Goroutines vs. Node.js Event Loop: Scaling I/O-Bound Microservices Under High Load
  • Elixir Phoenix vs. Go Gin: Concurrency Models and Fault Tolerance Under Peak Request Volume
  • Python Celery vs. Go Channels: Distributed Task Queue Overhead and Memory Reliability

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (806)
  • Debugging & Troubleshooting (584)
  • Security & Compliance (543)
  • SEO & Growth (491)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala