Rails has a mature, opinionated deployment story — and since Rails 7.1 it even ships a production Dockerfile out of the box. This guide covers a containerized deployment with a managed PostgreSQL database, Puma tuning, Sidekiq background jobs, and zero-downtime migrations.
What a production Rails app needs
- Puma as the application server, tuned for your core count.
- Precompiled assets (
assets:precompile) baked into the image. - A managed PostgreSQL database.
- Sidekiq (or Solid Queue) for background jobs.
RAILS_MASTER_KEYto decrypt credentials.
The Dockerfile
If you're on Rails 7.1+, rails new already generated a good multi-stage Dockerfile. Here's the shape of it:
FROM ruby:3.3-slim AS build
WORKDIR /rails
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev
COPY Gemfile Gemfile.lock ./
RUN bundle install --without development test
COPY . .
RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile
FROM ruby:3.3-slim
WORKDIR /rails
RUN apt-get update -qq && apt-get install -y libpq5 && rm -rf /var/lib/apt/lists/*
ENV RAILS_ENV=production
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails
EXPOSE 3000
CMD ["bin/rails", "server", "-b", "0.0.0.0"]The SECRET_KEY_BASE_DUMMY=1 trick lets assets:precompile run at build time without the real secret. Binding to 0.0.0.0 is required so the container accepts external traffic.
Tuning Puma
Puma uses workers (processes) and threads. Set them from the environment so you can tune without rebuilding:
# config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
preload_app!A reasonable starting point is WEB_CONCURRENCY = number of CPU cores and RAILS_MAX_THREADS = 5. Your database connection pool (in database.yml) must be at least as large as RAILS_MAX_THREADS, or threads will wait on connections.
Managed PostgreSQL
Configure database.yml to read from DATABASE_URL:
production:
adapter: postgresql
url: <%= ENV["DATABASE_URL"] %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>Run migrations as a release step:
bin/rails db:migrateFor zero-downtime, use the strong_migrations gem to catch unsafe operations (like adding a NOT NULL column without a default) before they lock your tables in production.
Credentials and secrets
Rails encrypts config/credentials.yml.enc and decrypts it with RAILS_MASTER_KEY. Set that key as a secret in production — never commit config/master.key. Everything else (third-party API keys) can live in encrypted credentials or plain env vars.
Background jobs with Sidekiq
Sidekiq needs Redis and runs as a separate process:
bundle exec sidekiq -C config/sidekiq.ymlRun it as its own service/container pointed at the same codebase and a Redis instance. If you'd rather avoid Redis, Rails 8's Solid Queue backs jobs with your existing database.
Deploying on PandaStack
- 1Create a PostgreSQL (14.x or 16.x) database.
DATABASE_URLis injected automatically. - 2Add a Redis instance if you use Sidekiq.
- 3Connect your repo as a container app; the generated Dockerfile is detected and built via rootless BuildKit.
- 4Set
RAILS_MASTER_KEY,RAILS_ENV=production, andWEB_CONCURRENCYin the dashboard. - 5Add
bin/rails db:migrateas a release command and run Sidekiq as a second service.
You get automatic SSL, live build logs, rollbacks, and deploy history.
| Concern | Setting |
|---|---|
| App server | Puma (WEB_CONCURRENCY, RAILS_MAX_THREADS) |
| Database | Managed PostgreSQL (DATABASE_URL) |
| Pool size | >= RAILS_MAX_THREADS |
| Jobs | Sidekiq + Redis, or Solid Queue |
| Secrets | RAILS_MASTER_KEY |
Common pitfalls
- Pool smaller than thread count — threads stall waiting for DB connections.
- Missing
RAILS_MASTER_KEY— the app boots then crashes decrypting credentials. - Binding to
localhost— no external traffic reaches the container. - Running migrations on boot across replicas — race conditions; run them once as a release step.
- Unsafe migrations — adding indexes non-concurrently can lock big tables; use
algorithm: :concurrently.
References
- Rails Docker guide: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
- Puma configuration: https://github.com/puma/puma#configuration
- Sidekiq deployment: https://github.com/sidekiq/sidekiq/wiki/Deployment
- strong_migrations gem: https://github.com/ankane/strong_migrations
- Rails credentials & secrets: https://guides.rubyonrails.org/security.html#custom-credentials
---
PandaStack's free tier gives you a container app, managed PostgreSQL, and Redis with DATABASE_URL auto-wired — ideal for Rails plus Sidekiq. Push your repo and it runs: https://dashboard.pandastack.io