Deploy Ruby on Rails to Production: Step-by-Step with Git Push
Bundle install, asset precompile, db:migrate — the complete Rails production deployment with git push.
In This Guide
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_BASEorRAILS_MASTER_KEYset - [ ]
config.force_ssl = truein production.rb - [ ]
config.session_storeuses:cookie_storewithsecure: 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 = truehandles 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 RequiredPowered by WHMCompleteSolution