Deploying Rails to Production: A Complete Guide

You built your Rails app. Now get it online. Step-by-step guide for Fly.io, Render, Heroku, and Kamal. Including database setup, environment variables, asset precompilation, and post-deployment checks.

Deploying Rails to Production: A Complete Guide

You built your Rails app. It works beautifully on your laptop.

Now you need to get it online.

The options are overwhelming. Heroku? Fly.io? Render? AWS? Kamal? Each one promises something different. Each one has its own quirks.

Let me cut through the noise. Here's how to deploy Rails to production, with real examples and honest trade-offs.


The Short Answer

Platform Best For Monthly Cost (Minimum) Complexity
Fly.io Most apps $5-15 Low
Render Simplicity $7-19 Very Low
Heroku Legacy apps, add-ons $25+ Low
Kamal + VPS Control, multiple apps $10-20 Medium
AWS ECS Enterprise scale $30+ High

For most developers, start with Fly.io or Render. They're simple, affordable, and handle everything you need.


Part 1: What You Need Before Deploying

Before you deploy anything, make sure your app is ready.

The Production Checklist

MARKDOWN
- [ ] Database uses PostgreSQL (not SQLite)
- [ ] Secrets stored in environment variables
- [ ] Assets precompile correctly
- [ ] Error tracking configured (Sentry, Honeybadger)
- [ ] Logging outputs to STDOUT
- [ ] Health check endpoint exists
- [ ] SSL enabled (most platforms do this automatically)
- [ ] Database migrations run automatically on deploy

Environment Variables You'll Need

BASH
# Required for most Rails apps
SECRET_KEY_BASE=your_long_random_string
DATABASE_URL=postgresql://user:pass@host/db
RAILS_ENV=production
RAILS_LOG_TO_STDOUT=true

# Payment processing
STRIPE_PUBLIC_KEY=pk_live_xxx
STRIPE_SECRET_KEY=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx

# Email (SendGrid, Mailgun, etc.)
SMTP_ADDRESS=smtp.sendgrid.net
SMTP_USERNAME=apikey
SMTP_PASSWORD=your_sendgrid_api_key
SMTP_DOMAIN=yourdomain.com

# Error tracking
SENTRY_DSN=https://xxx@sentry.io/xxx

The Health Check Endpoint

RUBY
# config/routes.rb
get "/up", to: "health#show"
RUBY
# app/controllers/health_controller.rb
class HealthController < ApplicationController
  def show
    # Check database connection
    ActiveRecord::Base.connection.execute("SELECT 1")

    # Check Redis if used
    Redis.current.ping if defined?(Redis)

    render plain: "OK", status: :ok
  rescue => e
    render plain: "ERROR: #{e.message}", status: :internal_server_error
  end
end

Fly.io runs your app on servers close to your users. They handle SSL, scaling, and global distribution.

Why Choose Fly.io

Pros Cons
Simple deployment PostgreSQL requires separate app
Global load balancing Learning curve for advanced features
Free for small apps (3 shared VMs) CLI-only (no web dashboard for deploy)
Built-in PostgreSQL option
Great documentation

Step-by-Step Deployment

1. Install Fly CLI

BASH
# Mac
brew install flyctl

# Linux
curl -L https://fly.io/install.sh | sh

# Windows (WSL)
curl -L https://fly.io/install.sh | sh

2. Sign in

BASH
fly auth signup
# or
fly auth login

3. Launch your app

BASH
fly launch

This creates a fly.toml file with your app configuration.

4. Set up PostgreSQL (optional but recommended)

BASH
fly postgres create

This creates a separate PostgreSQL app. Connect your Rails app to it.

5. Set environment variables

BASH
fly secrets set SECRET_KEY_BASE=$(rails secret)
fly secrets set RAILS_MASTER_KEY=$(cat config/master.key)
fly secrets set SENTRY_DSN=your_dsn

6. Deploy

BASH
fly deploy

7. Open your app

BASH
fly open

The fly.toml File

TOML
# fly.toml
app = "myapp"
primary_region = "iad"

[build]
  [build.args]
    NODE_VERSION = "18"
    RUBY_VERSION = "3.2.0"

[env]
  RAILS_ENV = "production"
  RAILS_LOG_TO_STDOUT = "true"
  RAILS_SERVE_STATIC_FILES = "true"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = false
  auto_start_machines = true
  min_machines_running = 1

[[services]]
  internal_port = 3000
  protocol = "tcp"

  [[services.ports]]
    port = 80
    handlers = ["http"]

  [[services.ports]]
    port = 443
    handlers = ["tls", "http"]

Common Fly.io Commands

BASH
# View logs
fly logs

# SSH into the machine
fly ssh console

# Run rails console
fly ssh console -C "rails console"

# Run migrations
fly ssh console -C "rails db:migrate"

# Scale to multiple machines
fly scale count 2

# Deploy with specific version
fly deploy --image "registry.fly.io/myapp:latest"

# List secrets
fly secrets list

Part 3: Option 2 - Render

Render is the easiest deployment option. No configuration files. No CLI learning curve.

Why Choose Render

Pros Cons
Web-based deployment More expensive than Fly.io
Automatic HTTPS Less global distribution
Built-in PostgreSQL with auto-backups Fewer advanced features
No YAML configuration
Great developer experience

Step-by-Step Deployment

1. Push your code to GitHub

BASH
git push origin main

2. Create a new Web Service on Render

  • Go to dashboard.render.com
  • Click New +Web Service
  • Connect your GitHub repository
  • Select Ruby as the environment

3. Configure the service

Setting Value
Name myapp
Root Directory (leave blank)
Environment Ruby
Build Command bundle install && bundle exec rake assets:precompile
Start Command bundle exec puma -C config/puma.rb

4. Add PostgreSQL

  • Click New +PostgreSQL
  • Name it myapp-db
  • Choose your plan (Free tier has 1GB storage)

5. Set environment variables

In your Web Service settings:

CODE
RAILS_ENV=production
SECRET_KEY_BASE=<generate with `rails secret`>
RAILS_MASTER_KEY=<your master key>
DATABASE_URL=<copied from PostgreSQL dashboard>

6. Deploy

Render automatically deploys when you push to GitHub.

Build and Start Commands

If you need custom commands:

BASH
# Build command
bundle install
bundle exec rake assets:precompile
bundle exec rake db:migrate

# Start command
bundle exec puma -C config/puma.rb

Part 4: Option 3 - Kamal (For Control)

Kamal deploys to your own VPS (DigitalOcean, Hetzner, AWS). You keep full control.

Why Choose Kamal

Pros Cons
Full control over infrastructure More complex setup
Lower costs at scale You manage servers
Deploy to any VPS No built-in monitoring
Zero downtime deploys
Uses your existing Dockerfile

Step-by-Step Deployment

1. Install Kamal

BASH
gem install kamal

2. Set up a VPS

Create a VPS on DigitalOcean, Hetzner, or any provider. Ubuntu 22.04 recommended.

3. Initialize Kamal

BASH
kamal init

This creates config/deploy.yml.

4. Configure deploy.yml

YAML
# config/deploy.yml
service: myapp
image: myapp
servers:
  web:
    - 123.123.123.123
  workers:
    - 123.123.123.123

registry:
  server: ghcr.io
  username: your_github_username
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  clear:
    RAILS_ENV: production
    RAILS_LOG_TO_STDOUT: true
  secret:
    - DATABASE_URL
    - SECRET_KEY_BASE
    - SENTRY_DSN

volumes:
  - "storage:/rails/storage"

asset_path: /rails/public/assets

builder:
  arch: amd64

ssh:
  user: root

5. Prepare the server

BASH
kamal server bootstrap

6. Deploy

BASH
kamal deploy

Kamal Commands

BASH
# Deploy
kamal deploy

# Rollback
kamal rollback

# Run rails console
kamal app exec -i "rails console"

# Run migrations
kamal app exec -i "rails db:migrate"

# View logs
kamal app logs

# Restart app
kamal app restart

# Destroy everything
kamal teardown

Part 5: Option 4 - Heroku (Legacy)

Heroku was the standard for years. It still works well, but it's expensive.

Why Choose Heroku

Pros Cons
Battle-tested Expensive for production
Excellent add-ons Sleeps after 30 minutes (free tier)
Simple git push deploy Limited resources at low tiers
Great documentation

Step-by-Step Deployment

1. Install Heroku CLI

BASH
# Mac
brew install heroku/brew/heroku

# Ubuntu
curl https://cli-assets.heroku.com/install.sh | sh

2. Login

BASH
heroku login

3. Create app

BASH
heroku create myapp

4. Add PostgreSQL

BASH
heroku addons:create heroku-postgresql:mini

5. Set environment variables

BASH
heroku config:set SECRET_KEY_BASE=$(rails secret)
heroku config:set SENTRY_DSN=your_dsn

6. Deploy

BASH
git push heroku main

7. Run migrations

BASH
heroku run rails db:migrate

Heroku Commands

BASH
# View logs
heroku logs --tail

# Open console
heroku run rails console

# Run migrations
heroku run rails db:migrate

# Restart
heroku restart

# Scale dynos
heroku ps:scale web=2

# Open app
heroku open

Part 6: Database Migrations

Migrations must run automatically on deploy or you'll forget.

Fly.io Migrations

Add to Dockerfile or use a release command:

DOCKERFILE
# Dockerfile
FROM ruby:3.2-slim

# ... other config ...

# Run migrations before starting the server
RUN bundle exec rake db:migrate

CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

Or use a separate release command in fly.toml:

TOML
[deploy]
  release_command = "bundle exec rake db:migrate"

Render Migrations

Add to your Build Command:

CODE
bundle install && bundle exec rake assets:precompile && bundle exec rake db:migrate

Kamal Migrations

BASH
kamal app exec -i "rails db:migrate"

Heroku Migrations

Heroku runs rails db:migrate automatically after deploy if you have a release phase in Procfile:

CODE
# Procfile
web: bundle exec puma -C config/puma.rb
release: bundle exec rake db:migrate

Part 7: Database Backups

You will lose data eventually. Plan for it.

PostgreSQL Backups

Fly.io: Backups are automatic. Download via:

BASH
fly pg backup list
fly pg backup download <id>

Render: Daily backups included. Download from dashboard.

Heroku: PG Backups add-on (free for basic plan).

BASH
heroku pg:backups:capture
heroku pg:backups:download

Kamal + VPS: Set up your own cron job:

BASH
# /etc/cron.daily/pg_backup
#!/bin/bash
pg_dump myapp_production | gzip > /backups/myapp_$(date +%Y%m%d).sql.gz

Part 8: Asset Precompilation

Rails must compile CSS, JavaScript, and images for production.

Standard Setup

RUBY
# config/environments/production.rb
config.assets.compile = false  # Always false in production
config.assets.digest = true
config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?

Precompile Locally (For Some Deployments)

BASH
RAILS_ENV=production bundle exec rake assets:precompile
git add public/assets
git commit -m "Precompile assets"
git push

Most platforms run:

BASH
bundle exec rake assets:precompile

During the build phase.

If Assets Don't Load

Check these:

BASH
# Verify files exist
ls public/assets/

# Check Rails is serving them
curl -I https://yourapp.com/assets/application-xxx.css

# If not, set environment variable
RAILS_SERVE_STATIC_FILES=true

Part 9: Environment-Specific Config

Don't hardcode anything. Use environment variables.

Rails Credentials vs ENV

Method Best For
ENV.fetch("KEY") Values that change per environment
Rails.application.credentials Secrets committed to repo

Example Config

RUBY
# config/database.yml
production:
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  url: <%= ENV["DATABASE_URL"] %>

# config/storage.yml
amazon:
  service: S3
  access_key_id: <%= ENV["AWS_ACCESS_KEY_ID"] %>
  secret_access_key: <%= ENV["AWS_SECRET_ACCESS_KEY"] %>
  region: <%= ENV["AWS_REGION"] %>
  bucket: <%= ENV["AWS_BUCKET"] %>

# config/initializers/sentry.rb
Sentry.init do |config|
  config.dsn = ENV["SENTRY_DSN"]
end

Part 10: Post-Deployment Checklist

After your app is live, verify everything works.

Smoke Tests

BASH
# Check home page
curl -I https://yourapp.com

# Check health endpoint
curl https://yourapp.com/up

# Check static assets
curl -I https://yourapp.com/assets/application.css

# Check database
curl https://yourapp.com/api/health/db

Monitoring to Set Up

Tool Purpose Free Tier
Sentry Error tracking 5,000 events/month
Scout Performance monitoring Trial only
Uptime Robot Uptime monitoring 5 monitors free
Logtail Log management 5GB/month
Better Stack Uptime + logs 1 monitor free

Initial Tasks

BASH
# Run migrations if not automatic
rails db:migrate

# Seed data if needed
rails db:seed

# Precompile assets if not automatic
rails assets:precompile

# Warm up caches
rails runner "Product.preload_categories"

The Deployment Comparison Table

Feature Fly.io Render Heroku Kamal
Free tier Yes No Yes (sleeps) N/A
Starting price $5 $7 $25 $10 (VPS)
SSL Auto Auto Auto Manual
PostgreSQL Optional (extra $5) Included Add-on ($9) Self-managed
Redis Optional Optional Add-on Self-managed
Global deployment Yes No No Optional
Zero downtime Yes Yes Yes (paid) Yes
Console access fly ssh console Built-in heroku run kamal app exec
Learning curve Low Very Low Low Medium

My Recommendation

Start with Fly.io if you want global deployment and low cost.

Start with Render if you want the simplest possible setup.

Use Kamal when you need control and have the skills.

Use Heroku only if you already know it or need its add-ons.


Quick Start Commands

Fly.io (What I recommend for most)

BASH
fly launch
fly postgres create
fly secrets set SECRET_KEY_BASE=$(rails secret)
fly deploy

Render

Push to GitHub → Connect on render.com → Add PostgreSQL → Deploy

Kamal (For your own server)

BASH
kamal init
kamal server bootstrap
kamal deploy

Summary

Deploying Rails doesn't have to be hard.

Start simple. Use Fly.io or Render. They handle SSL, databases, and scaling.

Don't spend weeks researching deployment platforms. Pick one. Deploy. Get back to building your app.

Your users don't care where you host. They care that your app works.

Make it work. Make it fast. Make it reliable.

Then go build the next thing.

DO

Derrick Orare

Software Engineer

Discussion

0 Comments

Questions, feedback or corrections are welcome.

💬

No discussion yet.

Be the first to share your thoughts.

Leave a comment