Back to Blog
Tutorial11 min read2026-07-03

How to Deploy a Ruby on Rails App with PostgreSQL

Deploy Rails to production with a managed PostgreSQL database: a tuned Dockerfile, Puma config, asset precompilation, migrations, Sidekiq workers, and the env vars that keep it secure.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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_KEY to 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:migrate

For 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.yml

Run 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

  1. 1Create a PostgreSQL (14.x or 16.x) database. DATABASE_URL is injected automatically.
  2. 2Add a Redis instance if you use Sidekiq.
  3. 3Connect your repo as a container app; the generated Dockerfile is detected and built via rootless BuildKit.
  4. 4Set RAILS_MASTER_KEY, RAILS_ENV=production, and WEB_CONCURRENCY in the dashboard.
  5. 5Add bin/rails db:migrate as a release command and run Sidekiq as a second service.

You get automatic SSL, live build logs, rollbacks, and deploy history.

ConcernSetting
App serverPuma (WEB_CONCURRENCY, RAILS_MAX_THREADS)
DatabaseManaged PostgreSQL (DATABASE_URL)
Pool size>= RAILS_MAX_THREADS
JobsSidekiq + Redis, or Solid Queue
SecretsRAILS_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

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also