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:migrateUse 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:startIf 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 sidekiqEither 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 mainSet 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_URLinjected 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.