Rails Deploy · 2025

Deploy Ruby on Rails to Production: Step-by-Step with Git Push

Updated April 2025 · 10 min read

Bundle install, asset precompile, db:migrate — the complete Rails production deployment with git push.

HomeBlog › Deploy Ruby on Rails to Production: Step-by-Step with Git Push

Deploy Ruby on Rails to Production: Step-by-Step with Git Push (2025)

Deploying Rails to production has historically been the source of countless hours of frustration: Capistrano configurations, Passenger vs Puma decisions, Nginx setup, asset pipeline compilation, secret key management, database connection pooling. It was complex enough that entire services (Heroku, Engine Yard) were built specifically to abstract it.

With git-based deployment on a managed platform, none of that configuration is required. Here's the complete, step-by-step guide to get a Rails app live with git push as the only deployment command.

What Rails Git Deployment Looks Like in Practice

# Write code
rails generate controller Products index show
# ... implement ...
git add .
git commit -m "Add Products controller"
git push apexweave main
# Deployment runs automatically:
# bundle install → asset precompile → db:migrate → Puma restart
# App is live in ~60–120 seconds

Step 1: Prepare Your Rails App for Production

1.1 Verify Gemfile and Gemfile.lock are committed

Gemfile.lock must be committed. It pins exact gem versions, ensuring the server installs the same gems you tested locally.

git add Gemfile Gemfile.lock
git status  # both should be tracked

1.2 Set up config/environments/production.rb

Rails ships with a sensible production environment config. Key settings to verify:

# config/environments/production.rb

Rails.application.configure do
  # Code is not reloaded between requests
  config.cache_classes = true
  config.eager_load = true

  # Full error reports disabled, caching enabled
  config.consider_all_requests_local = false
  config.action_controller.perform_caching = true

  # Disable serving static files (Nginx handles this)
  # OR enable if using container deployment without separate Nginx:
  config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?

  # Asset pipeline
  config.assets.compile = false  # Precompile, don't compile on-the-fly
  config.assets.digest = true    # Fingerprint assets for cache busting

  # Force HTTPS
  config.force_ssl = true

  # Log level
  config.log_level = :info

  # Use a different logger for structured logging
  config.log_formatter = ::Logger::Formatter.new

  # Send deprecation notices as exceptions
  config.active_support.deprecation = :notify

  # Active Record session store
  config.session_store :cookie_store, key: '_app_session', secure: true
end

1.3 Configure database for production

# config/database.yml
production:
  adapter: postgresql  # or mysql2
  url: <%= ENV['DATABASE_URL'] %>
  pool: <%= ENV.fetch('RAILS_MAX_THREADS') { 5 } %>
  timeout: 5000

Using DATABASE_URL from an environment variable is the cleanest approach — no hardcoded credentials.

1.4 Configure Puma (production web server)

Rails includes Puma by default. Verify config/puma.rb:

# config/puma.rb
max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
threads min_threads_count, max_threads_count

worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development'

port        ENV.fetch('PORT', 3000)
environment ENV.fetch('RAILS_ENV', 'development')

# Workers for multi-process mode (use 1 per CPU core)
workers ENV.fetch('WEB_CONCURRENCY', 2)

preload_app!

on_worker_boot do
  # Required for preload_app! with Active Record
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end

plugin :tmp_restart

1.5 Create .gitignore

# Rails standard
.bundle/
log/*.log
tmp/
.env
*.env
config/master.key
config/credentials/*.key
public/assets
public/uploads
/storage
.byebug_history
.idea/
.DS_Store

Critical exclusions:
- .env — never commit credentials
- config/master.key — Rails encrypted credentials key (store separately as env var)
- public/assets — generated by assets:precompile
- /storage — Active Storage files (use S3 in production)

Step 2: Configure Your Ruby Version

Create .ruby-version in your project root:

3.3.0

Set on ApexWeave:

apexweave env:set your-app.apexweaveapp.com APEXWEAVE_STACK=ruby:3.3

Available: ruby:3.1, ruby:3.2, ruby:3.3 (default: ruby:3.3)

Step 3: Set Environment Variables

# Rails configuration
apexweave env:set your-app.apexweaveapp.com RAILS_ENV=production
apexweave env:set your-app.apexweaveapp.com RAILS_SERVE_STATIC_FILES=true  # serve assets from Puma
apexweave env:set your-app.apexweaveapp.com RAILS_LOG_TO_STDOUT=true       # log to stdout for platform capture

# Credentials — choose ONE approach:

# Approach 1: Rails encrypted credentials (recommended for Rails 6+)
apexweave env:set your-app.apexweaveapp.com RAILS_MASTER_KEY=your-master-key-value
# The master key is in config/master.key — copy its content here

# Approach 2: Direct env vars (simpler)
apexweave env:set your-app.apexweaveapp.com SECRET_KEY_BASE=$(openssl rand -hex 64)

# Database
apexweave env:set your-app.apexweaveapp.com DATABASE_URL=postgres://username:password@dns.apexweaveapp.com:5432/myapp_production

# Performance
apexweave env:set your-app.apexweaveapp.com RAILS_MAX_THREADS=5
apexweave env:set your-app.apexweaveapp.com WEB_CONCURRENCY=2

# Email
apexweave env:set your-app.apexweaveapp.com SMTP_HOST=smtp.mailgun.org
apexweave env:set your-app.apexweaveapp.com SMTP_USERNAME=postmaster@mg.yourdomain.com
apexweave env:set your-app.apexweaveapp.com SMTP_PASSWORD=your-mailgun-key

# Third-party
apexweave env:set your-app.apexweaveapp.com STRIPE_SECRET_KEY=sk_live_...
apexweave env:set your-app.apexweaveapp.com S3_BUCKET=your-production-bucket
apexweave env:set your-app.apexweaveapp.com AWS_ACCESS_KEY_ID=AKIA...
apexweave env:set your-app.apexweaveapp.com AWS_SECRET_ACCESS_KEY=xxx

# Verify
apexweave env:list your-app.apexweaveapp.com

Step 4: Configure Build and Deploy Commands

In ApexWeave dashboard → Settings → Build Configuration:

Install Command:

bundle install --without development test

Build Command:

bundle exec rails assets:precompile

This compiles and fingerprints all JavaScript, CSS, and image assets. Required for production — without it, the asset pipeline serves uncompressed, non-fingerprinted files.

Start Command:

bundle exec puma -C config/puma.rb

Post-Deployment Hook:

bundle exec rails db:migrate

The db:migrate hook runs database migrations automatically on every deployment. Migrations that have already run are skipped automatically — only pending migrations execute.

For more complex deployment hooks:

bundle exec rails db:migrate && bundle exec rails db:seed

(Only run db:seed if your seeds are idempotent — i.e., safe to run multiple times without creating duplicate data.)

Step 5: Deploy

# Add ApexWeave remote
git remote add apexweave https://git.apexweaveapp.com/your-username/your-app.git

# Push
git push apexweave main

# Watch deployment
apexweave deploy your-app.apexweaveapp.com --follow

Expected successful output:

Pulling commit: 8bc4e1d
Running: bundle install --without development test
Fetching gem metadata from https://rubygems.org/...
Bundle complete! 47 Gemfile dependencies, 195 gems now installed.

Running: bundle exec rails assets:precompile
I, [2025-04-15T14:30:01.123456 #1234] INFO -- : Writing /app/public/assets/application-a1b2c3d4.css
I, [2025-04-15T14:30:03.456789 #1234] INFO -- : Writing /app/public/assets/application-d4e5f6a7.js
Assets precompiled successfully.

Running post-deployment hook:
  bundle exec rails db:migrate
  == 20250401123456 CreateProducts: migrating ==================================
  -- create_table(:products)
     -> 0.0423s
  == 20250401123456 CreateProducts: migrated (0.0424s) =========================

Running: bundle exec puma -C config/puma.rb
[4567] Puma starting in cluster mode...
[4567] * Puma version: 6.4.0
[4567] * Ruby version: ruby 3.3.0
[4567] * Workers: 2
[4567] * Threads: 5
[4567] * Listening on http://0.0.0.0:8080

Health check: 200 OK
Deployment complete (94 seconds)

Step 6: Verify and Manage Your Deployment

# Check the app responds
curl https://your-app.apexweaveapp.com

# View logs
apexweave logs your-app.apexweaveapp.com
apexweave logs your-app.apexweaveapp.com --follow

# Open Rails console
apexweave bash your-app.apexweaveapp.com
# Then inside the container:
bundle exec rails console

# Run one-off commands
apexweave run "bundle exec rails db:migrate:status" your-app.apexweaveapp.com
apexweave run "bundle exec rails runner 'puts User.count'" your-app.apexweaveapp.com
apexweave run "bundle exec rake cache:clear" your-app.apexweaveapp.com

Step 7: Create Admin User

apexweave run "bundle exec rails runner \"User.create!(email: 'admin@yourdomain.com', password: 'SecurePassword123!', admin: true)\"" your-app.apexweaveapp.com

Or via Rails console:

apexweave bash your-app.apexweaveapp.com
bundle exec rails console
> User.create!(email: 'admin@yourdomain.com', password: 'SecurePassword123!', role: :admin)

Active Storage in Production: Use S3

Rails Active Storage is for file uploads (avatars, documents, images). In a container deployment, local storage is ephemeral — files are lost on redeploy.

Configure S3:

# config/storage.yml
amazon:
  service: S3
  access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
  secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
  region: us-east-1
  bucket: <%= ENV['S3_BUCKET'] %>
# config/environments/production.rb
config.active_storage.service = :amazon

All uploaded files go to S3, persist across redeployments, and are served via S3 URLs (or CloudFront CDN for performance).

Background Jobs in Production

If your Rails app uses Active Job with Sidekiq:

Environment variables:

apexweave env:set your-app.apexweaveapp.com REDIS_URL=redis://:password@dns.apexweaveapp.com:6379/0

Gemfile:

gem 'sidekiq'
gem 'redis'

Config:

# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

For running the Sidekiq worker alongside Puma, you'll need a process supervisor. The simplest approach for single-container deployment is foreman:

# Procfile
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq -C config/sidekiq.yml

Start Command:

bundle exec foreman start

Common Rails Production Deployment Issues

ExecJS::RuntimeError during assets:precompile

Cause: Node.js runtime not available for JavaScript compilation in the asset pipeline.

Fix: If using the legacy Sprockets pipeline (not Propshaft/jsbundling):

# Ensure Node.js is available in your container
# Set APEXWEAVE_STACK to a Ruby version that includes Node.js
# Or switch to Propshaft/importmap for JS (recommended for Rails 7+)

For modern Rails apps using importmap-rails:

# Gemfile
gem 'importmap-rails'
# No Node.js required for asset compilation

ActionView::Template::Error: couldn't find file 'application'

Cause: Assets not precompiled, or ASSET_HOST misconfigured.

Fix: Ensure assets:precompile is in your Build Command and RAILS_SERVE_STATIC_FILES=true is set.

Database connection pool exhausted (ActiveRecord::PoolFullError)

Cause: RAILS_MAX_THREADS × WEB_CONCURRENCY > database connection limit.

Fix:
- Reduce RAILS_MAX_THREADS or WEB_CONCURRENCY
- Or increase your database's max connections

Rule: total connections used = RAILS_MAX_THREADS × WEB_CONCURRENCY. With 5 threads × 2 workers = 10 connections per dyno.

Bootsnap::CompileCache::Unwritable warning

Cause: tmp/cache directory not writable.

Fix: Not critical — add to ignore list or ensure tmp/ directory exists with correct permissions in your container.

Assets not loading (404s for CSS/JS)

Fix sequence:
1. Verify RAILS_SERVE_STATIC_FILES=true
2. Verify assets:precompile ran (check build log)
3. Check config.assets.prefix in production.rb matches your asset path
4. Clear asset cache: bundle exec rails assets:clobber then redeploy

Rails Production Security Checklist

  • [ ] RAILS_ENV=production
  • [ ] SECRET_KEY_BASE or RAILS_MASTER_KEY set
  • [ ] config.force_ssl = true in production.rb
  • [ ] config.session_store uses :cookie_store with secure: true
  • [ ] No credentials in Gemfile, code, or git history
  • [ ] Active Storage using S3 (not local disk)
  • [ ] Database not SQLite (PostgreSQL or MySQL for production)
  • [ ] config.log_level = :info (not :debug — excessive log output)
  • [ ] config.eager_load = true
  • [ ] Asset fingerprinting enabled
  • [ ] HSTS configured (config.force_ssl = true handles this in Rails 7+)

Everyday Rails Deployment Workflow

# Develop a new feature
rails generate migration AddStripeCustomerId to users stripe_customer_id:string
# ... implement the feature ...

# Test
bundle exec rspec

# Deploy
git add .
git commit -m "Add Stripe customer ID to users table"
git push apexweave main
# Migrations run automatically via post-deploy hook
# Assets recompile automatically

# Verify
apexweave logs your-app.apexweaveapp.com

Deploy your Rails app at apexweave.com/git-deployment.php — Ruby 3.3, automatic bundle install, asset precompilation, and migration hooks included.

Deploy Your App with Git Push

Automatic builds, environment variables, live logs, rollback, and custom domains. No server management required.

Deploy Free — No Card Required

Powered by WHMCompleteSolution