Scott's Recipes Logo

Bullet Proofing a Rails Deploy


Please note that all opinions are that of the author.


Last Updated On: 2025-11-26 06:51:47 -0500

I am now an almost 20 plus year veteran of the Rails development / deployment cycle. That means I’ve seen all the damn generations of this crap:

And I can, with confidence, say the following:

RAILS DEPLOYMENT IS BETTER IN 2025 BUT IT STILL SUCKS

One of the rules of any software development platform (and Rails is a platform) is TANSTAAFL: There ain’t no such thing as a free lunch. In other words, for everything a platform gives you, you lose something else. Rails gives you what I would argue is the easiest way to develop a web application but the cost for that is, well, the worst deployment experience you can ever have. We can argue about why this is the case but here are some thoughts:

Now I’d like to state that all of my Rails apps have 100% test coverage, a CI server and all the other bells and whistles – but they don’t – and I’d like to bet that your Rails apps aren’t perfect either.

So How You Bullet Proof a Rails Deployment

And, finally, we come to the thesis of this blog post, how you actually bullet proof a Rails deployment. There are four aspects:

1. Deploys Should Never Be Automated

This is a controversial point in a world where Heroku created the idea that deploys about to “git push” and magic happens. And, yes, I’ve worked on code bases where this works and it can be brilliant. But those environments were rarely scrappy, self funded startups. They were always places / code bases where you had these characteristics:

These days – and for most of my career – I operate at the other end of the spectrum:

2. Test Coverage

Test coverage in a dynamic language like Ruby is absolutely essential and I’ll defend that viewpoint to my very grave but as important as test coverage is – it can never catch everything. When you are run an engineering team of 1 or 2 there is always the chance that things happen. Just as an example, there is some weirdness in OSX where copying a filename can inadvertently, in my editor, lead to that being pasted into the current code window. Now sometimes that will cause an error but Rails is a swiss watch of complexity and that one thing, in the wrong file, can cause a significant issue. Here’s an example of a single character error which isn’t caught easily but can break the world:

<%=# add_spacer %>

3. Syntax Checking

Even if you have 100% test coverage on your Rails app, I would wager that you don’t have coverage on your Rake tasks. Test coverage on Rake tasks has always been problematic in my experience and the reason that matters is the way that Rails loads your application in production:

  1. All executable code in all directories is loaded when your Rails application is initialized.
  2. A syntax error in your Rake task can prevent your application from starting.

Here is a Rake task which you can incorporate as a first step in a deploy pipeline which syntax checks all your .rb files:

namespace :code do
  desc "Check Ruby files for syntax errors"
  task :syntax do
    failed = false
    Dir.glob("**/*.rb").each do |file|
      next if File.directory?(file)
      # Skip anything in the docs directory because it isn't a code path subject to execution
      next if file =~ /^docs\//
      result = `ruby -c #{file} 2>&1`
      unless result.include?("Syntax OK")
        puts "[ERROR] #{file}:\n#{result}"
        failed = true
      end
    end
    abort("Syntax errors found!") if failed
    puts "All Ruby files passed syntax check."
    
    failed = false
    Dir.glob("**/*.rake").each do |file|
      next if File.directory?(file)
      # Skip anything in the docs directory because it isn't a code path subject to execution
      next if file =~ /^docs\//
      result = `ruby -c #{file} 2>&1`
      unless result.include?("Syntax OK")
        puts "[ERROR] #{file}:\n#{result}"
        failed = true
      end
    end
    abort("Syntax errors found!") if failed
    puts "All Rake tasks passed syntax check."
  end
end

4. Post Deploy Testing

As noted above, I don’t believe in automated deploys. I strongly, strongly, strongly believe in the traditional build master approach to software development when you don’t have 100% test coverage, code reviews and rigorous engineering practices. In these environments, I believe that deploy should be a deliberate action that a person on the team should own responsibility for. It might be multiple people but:

  1. Someone has to push the deploy button.
  2. That deploy process has to happen with a human in the loop monitoring things.
  3. At the end of that deploy process, something should run which gives some level of sanity “ok at a bare minimum, this is good”.

Here’s a simple post-deploy testing rake task which can be baked into a deploy pipeline:

namespace :post_deploy do
  # bundle exec rake post_deploy:check --trace
  task :check => :environment do 
    agent = Mechanize.new
    agent.user_agent = "FuzzyBot/1.0 (+https://example.com/bot-info)"
    
    urls = []
    urls << "https://www.pollitify.com/pages/SoupForOurFamiliesGrants-press-release-soup-for-our-families"
    urls << "https://www.pollitify.com/grants"
    urls << "https://www.pollitify.com/"
    urls << "https://www.pollitify.com/events/"
    urls << "https://pollitify.com/events/u2dTFDo2-berkeley-rush-hour-resistance"
    urls << "https://www.pollitify.com/early"
    urls << "https://www.pollitify.com/news_feed_items"
    urls << "https://www.pollitify.com/posts"
    urls << "https://www.pollitify.com/users"
    urls << "https://www.pollitify.com/badges"
    urls << "https://www.pollitify.com/posts/new"
    urls << "https://www.pollitify.com/home/index?state_id=5"
    urls << "https://www.pollitify.com/polls/"
    urls << "https://www.pollitify.com/pages/"
    urls << "https://www.pollitify.com/user_logins/"
    urls << "https://www.pollitify.com/press/"
    urls << "https://www.pollitify.com/grants/"
    urls << "https://www.pollitify.com/mutualaid/"
    urls << "https://www.pollitify.com/mutual_aid/"
    urls << "https://www.pollitify.com/mutual-aid/"
    
    
    puts "========================================================="
    puts "HINT -- IF THE rake post_deploy:check"
    puts "fails then check the home page in logged out status"
    puts "========================================================="
    urls.each do |url|
      puts "About to get url: #{url}"
      page = agent.get(url)      
      if page.code != "200"
        raise "Error on: #{url}"
      else 
        puts "  Success!!!"
      end
    end
  end
end

This is not perfect by any means and it is exactly as simple as its brevity would suggest but it runs every single time on every one of our deploys and more than once it has caught something important.

Pulling it All Together

I use Kamal for deploy and I pull all this together with a simple shell script I run for every deploy:

#!/bin/bash
set -e
ECHO "STEP 1: Making Sure Rails Code Passes Syntax Check"
bin/rails code:syntax
# nvm use 22
# bin/rails test

ECHO "STEP 2: Running Deploy"
source .env.production

bundle exec kamal deploy

ECHO "STEP 3: Running Site Map Creation"
# Step 2: Find the running web container
WEB_CONTAINER=$(ssh root@1.2.3.4 "docker ps --filter 'name=SOMETHING-web' --format ''")

if [ -z "$WEB_CONTAINER" ]; then
  echo "Error: Could not find running web container"
  exit 1
fi

echo "  Running sitemap task inside container $WEB_CONTAINER..."

# Step 3: Run the sitemap rake task inside the container
ssh root@1.2.3.4 "docker exec -u rails $WEB_CONTAINER bundle exec rake sitemap:create"

echo "Sitemap generation complete."

unset DATABASE_URL

ECHO "STEP 4: Making Sure Site Still Works"
bundle exec rake post_deploy:check

A Closing Thought: Pay Attention

My final point on deploy is really, really simple: pay attention when you deploy. I’d argue that if we were in the middle ages, our map for a Rails deploy would include the words “here there be dragons”. Rails is still my favorite platform I’ve ever worked on but I’m just under no illusions when it comes to Rails deployment. Pay attention when you deploy because it can be very, very fragile.