Ruby on Rails Production Deployment: Complete Guide
Ruby on Rails powers many of the world's most successful web applications. Its convention-over-configuration philosophy makes development fast, but production deployments require careful Docker configuration, the Puma web server, asset compilation, and database migration management.
This guide covers a complete Rails production deployment on [PandaStack](https://pandastack.io).
Rails Production Architecture
A production Rails deployment typically consists of:
- Puma — the web server handling HTTP requests
- Sidekiq — background job processing (requires Redis)
- PostgreSQL — primary database
- Redis — Action Cable, caching, and Sidekiq queue
On PandaStack, you deploy the Rails web server as a container project and can run Sidekiq as a separate cronjob or container service.
Production Dockerfile
Rails 7+ ships with a built-in Dockerfile generator. Here is a production-ready version:
FROM ruby:3.3-alpine AS base
RUN apk add --no-cache build-base postgresql-dev nodejs yarn tzdata libffi-dev
WORKDIR /rails
ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT=development:test
FROM base AS deps
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs 4 --retry 3
FROM base AS assets
COPY --from=deps /usr/local/bundle /usr/local/bundle
COPY . .
RUN bundle exec rails assets:precompile
FROM ruby:3.3-alpine AS runner
RUN apk add --no-cache postgresql-client tzdata libffi
WORKDIR /rails
ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
ENV RAILS_LOG_TO_STDOUT=true
COPY --from=deps /usr/local/bundle /usr/local/bundle
COPY --from=assets /rails /rails
RUN addgroup --system rails && adduser --system --ingroup rails rails
RUN chown -R rails:rails db log storage tmp
USER rails
EXPOSE 3000
CMD ["./bin/rails", "server", "-b", "0.0.0.0"]Key environment variables:
RAILS_SERVE_STATIC_FILES=true— Puma serves precompiled assets without NginxRAILS_LOG_TO_STDOUT=true— logs to stdout for the platform to capture
Database Migrations via Startup Script
#!/bin/sh
# bin/docker-entrypoint
set -e
if [ "${*}" = "./bin/rails server -b 0.0.0.0" ]; then
./bin/rails db:migrate 2>/dev/null || true
fi
exec "${@}"COPY bin/docker-entrypoint /rails/bin/docker-entrypoint
RUN chmod +x /rails/bin/docker-entrypoint
ENTRYPOINT ["/rails/bin/docker-entrypoint"]Migrations run only when starting the web server, not on every container command.
Configuring pandastack.json
{
"type": "container",
"healthCheckPath": "/up"
}Rails 7.1+ ships with a built-in /up health check endpoint that verifies database connectivity. If you're on an older version, add a simple health route:
# config/routes.rb
get "/health", to: proc { [200, {}, [{ status: :ok }.to_json]] }Environment Variables
Set all secrets and configuration from [dashboard.pandastack.io](https://dashboard.pandastack.io):
RAILS_ENV=production
SECRET_KEY_BASE=your-128-char-hex-secret
DATABASE_URL=postgresql://user:pass@host:5432/myapp_prod
REDIS_URL=redis://redis.internal:6379
# Action Mailer
SMTP_HOST=smtp.mailgun.org
SMTP_USERNAME=your-user
SMTP_PASSWORD=your-password
SMTP_PORT=587
# Application config
APP_HOST=yourdomain.comGenerate SECRET_KEY_BASE locally:
bundle exec rails secretDatabase Configuration
Configure your database to read from DATABASE_URL:
# config/database.yml
production:
adapter: postgresql
encoding: unicode
url: <%= ENV['DATABASE_URL'] %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>Provision a managed PostgreSQL instance from the Databases section at [dashboard.pandastack.io](https://dashboard.pandastack.io).
Puma Configuration
# config/puma.rb
threads_count = ENV.fetch("RAILS_MAX_THREADS", 3).to_i
threads threads_count, threads_count
port ENV.fetch("PORT", 3000)
environment ENV.fetch("RAILS_ENV", "production")
workers ENV.fetch("WEB_CONCURRENCY", 2).to_i
preload_app!
on_worker_boot do
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end
before_fork do
ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
endRunning Sidekiq as a Cronjob
For background jobs, run Sidekiq as a separate process. On PandaStack, you can configure a cronjob that keeps Sidekiq running. Alternatively, deploy it as a separate container project without a web port, using the same Docker image with a different command override:
bundle exec sidekiq -C config/sidekiq.yml# config/sidekiq.yml
:concurrency: 5
:queues:
- [critical, 3]
- [default, 2]
- [low, 1]GitHub Integration and Deployment
Connect your GitHub repository from the PandaStack dashboard. Every push to your production branch automatically builds the Docker image (including asset precompilation) and deploys. For CLI deployments:
npm install -g @pandastack/cli
panda deployProduction Checklist
RAILS_ENV=productionandRAILS_LOG_TO_STDOUT=trueare setSECRET_KEY_BASEis set and secure (128+ hex characters)- Assets precompiled in the Docker build stage
- Database migrations run on startup via entrypoint script
- Puma configured with workers and threads
/upor/healthreturns HTTP 200- Redis provisioned for Sidekiq and caching
pandastack.jsonspecifies correcthealthCheckPath
Active Storage and Action Mailer in Production
Rails applications that handle file uploads (Active Storage) or send emails (Action Mailer) need additional configuration for production.
For Action Mailer, configure SMTP credentials as environment variables:
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV['SMTP_HOST'],
port: ENV['SMTP_PORT']&.to_i || 587,
user_name: ENV['SMTP_USERNAME'],
password: ENV['SMTP_PASSWORD'],
authentication: :plain,
enable_starttls_auto: true
}
config.action_mailer.default_url_options = { host: ENV['APP_HOST'], protocol: 'https' }Set SMTP_HOST, SMTP_USERNAME, SMTP_PASSWORD, and APP_HOST in the PandaStack dashboard. Always test email delivery in staging before going live — a misconfigured mailer silently drops emails in production.
Visit [docs.pandastack.io](https://docs.pandastack.io) for the full deployment reference.