Back to Blog
Tutorial11 min read2026-07-02

How to Deploy a Ruby on Rails App with PostgreSQL

Deploy Rails 7+ with a managed Postgres: an optimized Dockerfile, Puma tuning, assets precompiled, migrations as a release step, and Solid Queue or Sidekiq for background jobs.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Rails on a container platform

Rails 7+ ships with a sensible production Dockerfile out of the box (rails new generates one), so containerizing is no longer the chore it used to be. The work is in wiring a managed Postgres, tuning Puma, handling assets, and running migrations and background jobs cleanly. Here's the full path.

Step 1: The Dockerfile

A modern Rails Dockerfile is multi-stage and precompiles assets. The generated one is a great base; the essentials:

FROM ruby:3.3-slim AS build
WORKDIR /app
RUN apt-get update && apt-get install -y build-essential libpq-dev git
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local without 'development test' && bundle install
COPY . .
RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile

FROM ruby:3.3-slim
WORKDIR /app
RUN apt-get update && apt-get install -y libpq5 && rm -rf /var/lib/apt/lists/*
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /app /app
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

SECRET_KEY_BASE_DUMMY=1 lets assets:precompile run at build time without the real secret.

Step 2: Tune Puma for containers

Puma's defaults aren't ideal for constrained containers. Set workers and threads relative to your tier, and bind to the injected port:

# config/puma.rb
workers Integer(ENV.fetch('WEB_CONCURRENCY', 2))
threads_count = Integer(ENV.fetch('RAILS_MAX_THREADS', 5))
threads threads_count, threads_count
port ENV.fetch('PORT', 3000)
preload_app!

Match RAILS_MAX_THREADS to your Postgres pool size in database.yml — pool starvation is the classic Rails-in-production bug. On the free tier you have 50 DB connections, so size workers x threads accordingly.

Step 3: Wire the managed PostgreSQL

Provision a managed PostgreSQL (14.x or 16.x). Rails reads DATABASE_URL natively when set, overriding database.yml. PandaStack injects it when you attach the DB:

# config/database.yml — production stanza
production:
  url: <%= ENV['DATABASE_URL'] %>
  pool: <%= ENV.fetch('RAILS_MAX_THREADS', 5) %>

Step 4: Migrations as a release step

Never run migrations inside Puma boot on every replica — that races. Run them once per deploy:

bundle exec rails db:migrate

Use a dedicated release command in your deploy config, or a one-off job that runs before the new version takes traffic.

Step 5: Background jobs

Rails 8 ships Solid Queue (database-backed, no Redis needed), which is great when you want fewer moving parts:

bundle exec rails solid_queue:start

If you prefer Sidekiq, deploy a second container app with a managed Redis attached:

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

Either way, the worker is just another long-running container sharing the same database.

Step 6: Deploy

Connect your repo and push. PandaStack builds with rootless BuildKit in a K8s Job pod, pushes to Artifact Registry, and Helm-deploys with live logs:

git push origin main

Set env vars in the dashboard (RAILS_ENV=production, RAILS_MASTER_KEY or SECRET_KEY_BASE, RAILS_SERVE_STATIC_FILES=true if you serve assets from the app). Add your domain for automatic SSL.

Step 7: Logs and metrics

Set RAILS_LOG_TO_STDOUT=true so logs stream to the platform's live log view (self-hosted Elasticsearch). PandaStack captures server-side metrics and analytics without a client SDK, so you get request and resource visibility without instrumenting the app.

Production checklist

  • [ ] Multi-stage Dockerfile, assets precompiled at build
  • [ ] Puma workers/threads tuned to tier and DB pool
  • [ ] DATABASE_URL injected from managed Postgres
  • [ ] Migrations run as a release step, not on boot
  • [ ] Background jobs (Solid Queue or Sidekiq) as separate app
  • [ ] RAILS_LOG_TO_STDOUT=true

References

  • [Rails — Deploying with Docker (official guides)](https://guides.rubyonrails.org/getting_started.html)
  • [Puma configuration](https://github.com/puma/puma#configuration)
  • [Solid Queue](https://github.com/rails/solid_queue)
  • [Sidekiq wiki](https://github.com/sidekiq/sidekiq/wiki)

---

Rails plus a managed Postgres is a five-minute deploy once the Dockerfile and Puma config are dialed in. Attach the database, push, and let DATABASE_URL wire itself. Start free on PandaStack's [free tier](https://dashboard.pandastack.io) — 5 web services and a managed Postgres at $0/mo.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also