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
- [ ] 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
# 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
# config/routes.rb
get "/up", to: "health#show"
# 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
Part 2: Option 1 - Fly.io (Recommended for Most)
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
# 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
fly auth signup
# or
fly auth login
3. Launch your app
fly launch
This creates a fly.toml file with your app configuration.
4. Set up PostgreSQL (optional but recommended)
fly postgres create
This creates a separate PostgreSQL app. Connect your Rails app to it.
5. Set environment variables
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
fly deploy
7. Open your app
fly open
The fly.toml File
# 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
# 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
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:
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:
# 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
gem install kamal
2. Set up a VPS
Create a VPS on DigitalOcean, Hetzner, or any provider. Ubuntu 22.04 recommended.
3. Initialize Kamal
kamal init
This creates config/deploy.yml.
4. Configure deploy.yml
# 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
kamal server bootstrap
6. Deploy
kamal deploy
Kamal Commands
# 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
# Mac
brew install heroku/brew/heroku
# Ubuntu
curl https://cli-assets.heroku.com/install.sh | sh
2. Login
heroku login
3. Create app
heroku create myapp
4. Add PostgreSQL
heroku addons:create heroku-postgresql:mini
5. Set environment variables
heroku config:set SECRET_KEY_BASE=$(rails secret)
heroku config:set SENTRY_DSN=your_dsn
6. Deploy
git push heroku main
7. Run migrations
heroku run rails db:migrate
Heroku Commands
# 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
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:
[deploy]
release_command = "bundle exec rake db:migrate"
Render Migrations
Add to your Build Command:
bundle install && bundle exec rake assets:precompile && bundle exec rake db:migrate
Kamal Migrations
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:
# 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:
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).
heroku pg:backups:capture
heroku pg:backups:download
Kamal + VPS: Set up your own cron job:
# /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
# 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)
RAILS_ENV=production bundle exec rake assets:precompile
git add public/assets
git commit -m "Precompile assets"
git push
Precompile on Deploy (Recommended)
Most platforms run:
bundle exec rake assets:precompile
During the build phase.
If Assets Don't Load
Check these:
# 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
# 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
# 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
# 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)
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)
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.