Deployment

Objective

Deploy Rails application changes to production safely, with particular attention to database migrations that can cause downtime or data loss.

Inputs

Prerequisites

Workflow

Step 1: Migration Review

Review all pending migrations for safety concerns.

Use strong_migrations to detect dangerous operations:

bundle exec strong_migrations --check
Dangerous OperationSafe Alternative
Adding a column with a defaultAdd column, then backfill default.
value to a large table
Adding an index on a large tableUse CONCURRENTLY (see below).
Removing a columnMark as ignored first, remove later.
Renaming a columnAdd new column, backfill, drop old.
Changing column typeAdd new column, backfill, drop old.

Step 2: Deploy Migrations First

For zero-downtime deployments, run migrations before deploying new application code.

# Deploy step 1: Run migrations
bundle exec rails db:migrate

# Deploy step 2: Deploy application code
capistrano deploy  # or equivalent

This ensures the database schema is ready before the new code runs.

Step 3: Monitor Deployment

Watch for:

Step 4: Verify Deployment

Run smoke tests against the deployed version:

Step 5: Rollback if Needed

If issues are detected:

  1. Revert the application code to the previous version.
  2. Roll back migrations only if they are reversible and no data has been written.
  3. Investigate and document the issue before re-deploying.

Migration Patterns

Adding an Index Concurrently

class AddIndexToUsersOnEmail < ActiveRecord::Migration[7.1]
  disable_ddl_transaction!

  def change
    add_index :users, :email, algorithm: :concurrently
  end
end

Backfilling Data

Backfill in a separate Rake task, not in the migration:

# db*migrate*20260101000000_add_status_to_users.rb
class AddStatusToUsers < ActiveRecord::Migration[7.1]
  def change
    add_column :users, :status, :string, default: "active", null: false
  end
end

# lib*tasks*backfill.rake
namespace :backfill do
  desc "Backfill user status"
  task user_status: :environment do
    User.where(status: nil).find_each do |user|
      user.update!(status: "active")
    end
  end
end

Removing a Column Safely

  1. Mark the column as ignored in the model.
  2. Deploy.
  3. Remove the column in a subsequent migration.
  4. Deploy again.
class User < ApplicationRecord
  self.ignored_columns = %w[legacy_column]
end

# Subsequent migration
class RemoveLegacyColumnFromUsers < ActiveRecord::Migration[7.1]
  def change
    remove_column :users, :legacy_column
  end
end

Checklist

Expected Outputs

Related Documents