Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
142: Zero-Downtime Deployment for Rails
I remember the first time I managed a production deploy for a growing Rails app. I did what most of us do early on: I SSH'd into the server, pulled the latest code, ran rails db:migrate, and restarted the Puma server. For a few seconds—maybe a minute—the site just hung. I saw a handful of 502 Bad Gateway errors in the logs. At the time, I figured a few seconds of downtime wasn't a big deal. But as the user base grows, those seconds become minutes, and those minutes become lost revenue and angry Slack messages from the product team.
The "Stop-and-Restart" Gamble
The naive approach to deployment is linear: stop the old version, update the database, and start the new version. The problem here is the "gap of death." During that window where the server is restarting, your users are hitting a brick wall. Even worse, if you're running a cluster of three servers, you might restart them one by one, but you still run into a synchronization nightmare with the database. Imagine you've just run a migration to rename user.full_name to user.display_name. The moment that migration finishes, every single server still running the old code will start crashing because they're still trying to query full_name, which no longer exists.
This is the fundamental tension of Rails deployments: your code and your schema are tightly coupled, but they cannot be updated at the exact same microsecond across multiple server instances.
Decoupling Schema from Logic
To get to true zero-downtime, you have to stop thinking of a "deploy" as a single event and start thinking of it as a multi-phase transition. The secret is ensuring that your database is always backward-compatible with the version of the code currently running in production. I generally push for a "Expand and Contract" pattern. Instead of renaming a column in one go, you do it in three separate deployments.
Let's say we're adding a profile_picture_url to our Users table. In a naive world, you'd add the column and immediately start using it in the views. In a zero-downtime world, you first deploy the migration to add the column. At this point, the new column exists, but the old code doesn't know about it—which is fine; it just ignores it. Then, you deploy the code that starts writing to that column. Finally, once you're sure everything is stable, you deploy any cleanup tasks, like adding a NOT NULL constraint or removing an old, redundant column.
# Phase 1: The Expansion
# Migration: Add column (safe, doesn't break old code)
add_column :users, :profile_picture_url, :string
# Phase 2: The Transition
# Code: Update User model to handle both old and new logic if necessary
# Deploy this to all servers.
# Phase 3: The Contraction
# Migration: Remove the old column or add constraints
remove_column :users, :old_avatar_path
Handling the Process Handoff
Even with a compatible schema, you still have the problem of the Ruby process itself restarting. If you just kill the process, you kill the active requests. This is why I recommend using a phased restart or a rolling update strategy. If you're using Puma, you can use "phased restarts" which replace workers one by one. If you're on Kubernetes or using an AWS Target Group, you use a rolling update: the orchestrator spins up a new "Green" container, waits for the health check to pass, and only then kills the "Blue" container.
The trade-off here is complexity. You're now managing three deploys instead of one, and you're spending more time thinking about the state of your database. It feels like overkill for a small app, but once you've had to wake up at 3 AM because a migration locked a table for ten minutes and took down the entire site, you'll realize that the overhead of backward-compatible changes is a very small price to pay for peace of mind.
📋 Practical Task
Exercise: Designing a Zero-Downtime Column Migration
You are tasked with renaming the billing_address column to shipping_address in a Orders table for a high-traffic e-commerce site. You cannot afford any downtime, and you cannot have the site crash while the new code is rolling out across the server cluster.
Write a detailed "Deployment Plan" in a text file or markdown document. Your plan should not be a single list of steps, but rather a breakdown of three distinct deployment phases. For each phase, specify:
- What the database migration does (if any).
- What the application code changes are.
- Why this specific phase prevents the app from crashing for users who are still hitting servers running the previous version of the code.
Constraint: You cannot use a "maintenance page." The app must remain fully operational throughout the entire transition.
There are no comments for now.