Skip to Content
Course content

142: Zero-Downtime Deployment for Rails

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.