all posts

The best Ruby on Rails hosting platforms in 2026

Ajay Kumar··10 min read

Most Rails hosting roundups are a list of platforms that can run `bundle exec puma`, which is all of them. The differences that matter show up in the parts nobody demos: whether the connection pool survives contact with Puma's threads-per-worker arithmetic, whether there is a place for Sidekiq or Solid Queue to live, whether `db:migrate` runs once at deploy time or once per instance, and whether asset precompilation can happen at build time without handing the build box your production credentials.

This is written for the Rails app that is running real traffic, has a Sidekiq queue somebody is quietly afraid of, and has been about to be rewritten in Go for six years. I build PandaStack, one of the options below — flagged where it comes up, and described with its trade-offs rather than its brochure.

Everything about third-party platforms here is deliberately qualitative — no prices, no tier limits, no throughput claims. Hosting pricing and capabilities change fast enough that any number written down in a blog post is wrong within a quarter. Check each vendor's current pricing and docs pages before you commit an app to one.

What makes Rails hosting different

If you have only ever deployed a stateless Node service, the surprising part of Rails is how much of the application lives outside the request. Rails assumes a machine, not an invocation.

A long-lived Puma process, not a request-scoped function

Puma is a threaded server, usually run in clustered mode: a master process forks workers, each worker runs a thread pool. The whole design assumes the process outlives the request — preloaded application code, a warm connection pool, an in-process cache, and Ruby's GC settling into a steady state after the first few hundred requests.

Serverless function platforms can technically run Rack, and every year somebody demonstrates it. It is a poor structural fit: boot cost is paid per cold invocation on a framework that eager-loads a lot of code, there is nowhere for Active Job workers or scheduled work to live, and every concurrent invocation opens its own database connections. Rails on functions is a stunt that occasionally becomes a production system, which is how it becomes somebody's incident.

Postgres, and pool maths that actually adds up

Rails opens one database connection per thread, per process. That is the whole rule, and it is where most Rails-on-a-new-platform outages come from. Your peak connection count is roughly `puma_workers × RAILS_MAX_THREADS`, plus Sidekiq's concurrency in every worker process, plus whatever a console or a rake task grabs. Multiply that by the number of instances you scale to and compare it against the database's `max_connections` before you ship, not after.

# Pool sanity check — do this arithmetic before the deploy, not during the incident.
WEB_INSTANCES=2
PUMA_WORKERS=3
RAILS_MAX_THREADS=5           # DB pool per process must be >= this
SIDEKIQ_INSTANCES=1
SIDEKIQ_CONCURRENCY=10        # Sidekiq opens one connection per thread too

WEB_CONNS=$(( WEB_INSTANCES * PUMA_WORKERS * RAILS_MAX_THREADS ))
JOB_CONNS=$(( SIDEKIQ_INSTANCES * (SIDEKIQ_CONCURRENCY + 2) ))
echo "peak connections ≈ $(( WEB_CONNS + JOB_CONNS )) (+ consoles, + rake tasks)"

# Then compare against the server, not against your optimism:
psql "$DATABASE_URL" -c 'SHOW max_connections;'
psql "$DATABASE_URL" -c 'SELECT count(*) FROM pg_stat_activity;'

Two failure modes follow from getting this wrong. If `pool` in `database.yml` is smaller than `RAILS_MAX_THREADS`, threads queue inside your own process and you see `ActiveRecord::ConnectionTimeoutError` under load while the database sits idle. If the total across instances exceeds `max_connections`, new processes fail to boot at all — during a deploy, which is the worst possible moment. A transaction-mode pooler in front of Postgres fixes the second one, at the price of disabling prepared statements and losing session-scoped features. There's a longer treatment of the trade-offs at /blog/postgres-connection-pooling-explained.

If you put PgBouncer in transaction mode in front of Rails, set `prepared_statements: false` in database.yml. Otherwise you get intermittent `prepared statement "a1" already exists` errors that only appear under concurrency, survive a restart, and look like data corruption until you remember the pooler is there.

Background jobs and cron-ish work

Almost no Rails app is only a web process. Mailers, webhooks, imports, PDF generation, nightly rollups — all of that belongs in Active Job, backed by Sidekiq, Solid Queue, GoodJob, or similar. The platform question is simple: can you run a second long-lived process from the same codebase, with the same environment and the same release, without inventing a parallel deploy pipeline?

Scheduled work adds a second requirement: exactly one scheduler. Sidekiq-cron or Solid Queue's recurring tasks running on three replicas means three copies of the nightly billing job, which you discover from a customer, not from a dashboard. Either the platform gives you a singleton process type, or the scheduler needs its own database-backed lock. More on the process-model side of this at /blog/background-workers-alongside-web-apps.

# Procfile — the shape almost every Rails app ends up with.
# The point isn't the file format; it's that web, worker, and scheduler are
# three separate long-lived processes sharing one release.

web:     bundle exec puma -C config/puma.rb
worker:  bundle exec sidekiq -C config/sidekiq.yml
# Solid Queue instead of Sidekiq (Rails 8, no Redis):
# worker: bin/jobs
release: bin/rails db:migrate

# config/puma.rb — the two lines that decide your connection count
# workers Integer(ENV.fetch("WEB_CONCURRENCY", 3))
# threads 5, Integer(ENV.fetch("RAILS_MAX_THREADS", 5))

db:migrate runs once, before traffic

There is one correct place for migrations: a release step that runs after the build succeeds, before the new version takes traffic, exactly once. Rails takes an advisory lock so concurrent `db:migrate` calls don't interleave, which means the failure mode when you run migrations from every instance's entrypoint isn't corruption — it's every replica but one blocking on a lock during boot, health checks timing out, and a deploy that looks hung.

Ask a prospective platform two questions. Is there a pre-traffic release hook? And does a failing release step abort the deploy rather than letting the new version roll out anyway? Without both, a bad migration turns 'deploy blocked' into 'production crash-looping', and those are very different evenings. The compatible-migration discipline that makes this safe — expand, deploy, backfill, contract — is covered at /blog/zero-downtime-schema-migrations-on-deploy.

Asset precompilation and the SECRET_KEY_BASE dance

`rails assets:precompile` initializes the application, and an initialized Rails app in the production environment wants a `SECRET_KEY_BASE` and, if you use encrypted credentials, a `RAILS_MASTER_KEY`. This is the step where a perfectly good deploy dies with a message about a missing secret, on a build machine that has no business holding your production credentials in the first place.

Modern Rails gives you the escape hatch: set `SECRET_KEY_BASE_DUMMY=1` for the precompile step and Rails generates a throwaway key rather than demanding the real one. Assets get digested and fingerprinted, no secret leaves your runtime environment, and the build stays reproducible. If you are on Propshaft with importmaps you have less to compile than the Sprockets era, but you still need the digest manifest to exist before the first request.

Active Storage wants object storage

The default `local` Active Storage service writes uploads to disk. On any platform with an ephemeral filesystem, that means every deploy silently deletes user uploads — nothing errors, the app keeps working, and the symptom arrives weeks later as broken avatars. Point Active Storage at S3, GCS, or an S3-compatible bucket on day one, and check whether your platform has an easy path to one. This is the single most common Rails migration bug and it is entirely avoidable.

Rails 8, the Solid trio, and Kamal

Rails 8 changed the default answer for a lot of teams. Solid Queue, Solid Cache, and Solid Cable move jobs, caching, and websockets onto the database you already have, which deletes Redis from the dependency list for a large class of apps. Kamal deploys containers onto machines you rent, with a proxy in front that handles TLS and zero-downtime cutovers. Thruster sits in front of Puma for asset serving and compression.

Taken together, these make 'a boring VM you control' a genuinely competitive option again rather than a nostalgic one. That is a real shift: the managed-platform premium used to buy you Redis, a proxy, TLS, and a deploy tool. Now a chunk of it ships in the framework. It also means the honest comparison below is not just PaaS-versus-PaaS — it's managed platforms versus a Kamal setup that a competent team can stand up in an afternoon and then own forever.

The shape of a Rails deploy

Whatever platform you choose, the sequence is the same. The differences are in which steps the platform runs for you and which ones you wire up yourself.

# 1. Runtime version — pinned in the repo so build and runtime agree
echo "3.4.1" > .ruby-version

# 2. Build: install gems, compile assets without production secrets
bundle config set --local deployment true
bundle config set --local without 'development test'
bundle install
SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile

# 3. Release step — runs ONCE, after build, before traffic shifts.
#    Not in the entrypoint. Not manually. Once.
bundle exec rails db:migrate

# 4. Run: long-lived Puma, bound to 0.0.0.0 and the platform's port
RAILS_ENV=production \
RAILS_MAX_THREADS=5 \
WEB_CONCURRENCY=3 \
DATABASE_URL="postgres://user:pass@host:5432/app" \
bundle exec puma -C config/puma.rb -b tcp://0.0.0.0:$PORT
Bind Puma to 0.0.0.0, not 127.0.0.1. An app listening on localhost is reachable from inside the box and invisible to the platform's proxy, which produces a 502 with completely clean application logs. It is the most common 'works locally, broken in production' Rails deploy failure after ephemeral storage.

The realistic options

Heroku

The platform Rails deployment conventions were designed around. The Procfile, the release phase, `DATABASE_URL`, the twelve-factor config model — all of it is Heroku's vocabulary, which every other platform on this list adopted. The Ruby buildpack still handles bundler, asset precompilation, and process types with essentially no configuration, and the release phase does migrations correctly by default.

What it costs you now is less about the invoice than about gravity. Heroku's managed Postgres, add-on marketplace, and dyno model are comfortable and deeply sticky, and the platform's rate of change has been slower than the ecosystem around it. Teams tend to leave when they want more control over the machine, when the add-on ecosystem stops being the cheapest way to get a component, or when they need something that doesn't fit the dyno shape. Pick this if you want the least-surprising Rails deploy in existence and you value not thinking about it over owning it.

Render

The most direct spiritual successor to Heroku's developer experience, with managed Postgres, Redis, background worker services, and cron jobs as first-class objects, plus infrastructure-as-code via a repo-level blueprint file. Web service plus worker service plus a pre-deploy command covers the standard Rails topology without creativity.

The trade-off is that you are still on a managed container platform with the usual constraints: you get the machine the platform gives you, build behaviour is the platform's, and debugging a build that works locally means learning their build environment. Pick this if you want Heroku's shape with a more current platform underneath and you're happy staying inside a managed abstraction.

Fly.io

Fly runs your app as VMs close to users, with strong Rails affinity in the tooling — `fly launch` recognises a Rails app, generates a Dockerfile, and wires up a release command for migrations. Multi-region is the headline feature and it's real: you can put web processes near users and keep the primary database somewhere specific, with read replicas elsewhere.

That power is also the trade-off. Multi-region Rails means thinking about write forwarding, replica lag, and which requests can be served from a replica — genuine distributed-systems work that a single-region app never has to do. Postgres on Fly is closer to 'we run the primitives, you own the cluster' than to a fully managed database, which suits teams that want control and punishes teams that assumed managed. Pick this if latency to a geographically spread user base is a product requirement and you have someone who enjoys this class of problem.

Railway

The most pleasant first hour on this list. Connect a repo, get a service; add Postgres and Redis from a menu; environment variables reference each other across services so `DATABASE_URL` just appears. Multiple services from one repo makes web-plus-worker easy, and per-branch environments are a genuinely good fit for review apps.

The trade-off is that ease-of-use abstractions are the ones you eventually push against: less control over the underlying machine, and a usage-based model that rewards understanding what your app actually consumes. Pick this if developer velocity and preview environments matter more than deep infrastructure control, and re-evaluate when the app becomes load-bearing revenue.

Hatchbox, Kamal, and a VPS you own

The self-managed lane, in two flavours. Hatchbox provisions and manages Rails-shaped servers on a cloud account you own — the platform handles provisioning, deploys, and the Postgres/Redis/Puma/Sidekiq layout, while the servers and the bill stay yours. Kamal goes further: containers deployed onto machines you rent, a proxy handling TLS and zero-downtime cutovers, accessories for Postgres and Redis, and no platform in the middle at all.

This is the option with the best economics and the worst failure mode, and both should be stated plainly. You get the full machine, no per-service platform markup, and no abstraction between you and Linux. You also own kernel updates, backups you have actually tested restoring, monitoring, and the 3am page. With Rails 8's Solid trio removing Redis from many apps, the operational surface here is smaller than it was two years ago — but 'smaller' is not 'zero'. Pick this if you have at least one person who is comfortable owning servers and you would rather spend that person's time than a platform's margin.

AWS (ECS, Elastic Beanstalk)

The enterprise answer, chosen for reasons that are usually organisational rather than technical: you are already on AWS, compliance wants everything inside one account boundary, procurement has an existing agreement, or the security team needs IAM, VPC, and audit trails to be the primitives. Elastic Beanstalk is the PaaS-shaped front door; ECS (with Fargate or EC2) is where teams end up when Beanstalk's abstraction stops fitting. RDS handles Postgres properly, including read replicas, backups, and failover.

The trade-off is that nothing is Rails-shaped by default. Release-phase migrations become a one-off ECS task in your pipeline. Assets, secrets, log routing, and the load balancer are each their own service to configure. The total capability is the highest on this list and so is the setup cost — this is a platform-engineering project, not a deploy. Pick this if compliance or an existing AWS footprint makes it the answer, and staff it accordingly.

PandaStack

Mine, so read this section with the appropriate suspicion. PandaStack is git-driven app hosting on Firecracker microVMs: connect a repo, push to deploy, and each deploy builds into a fresh microVM and blue-green flips traffic to it once health checks pass. Runtime versions are resolved from the idiomatic files already in your repo — `.ruby-version` and `.tool-versions` are read by mise, so a pinned Ruby travels with the code and no Dockerfile is required. Managed Postgres 16 comes with a connection URL, and creating one takes 30–90 seconds. Background processes run alongside the web process in the same VM, so Sidekiq or Solid Queue is a second process rather than a second deploy pipeline.

The substrate is the actual differentiator. Every machine boots from a snapshot restore rather than a cold boot: a ~49ms restore step, roughly 179ms at p50 and 203ms at p99 end to end, against ~3s for a genuine first cold boot. That makes scale-to-zero economically honest instead of a marketing word — an idle app can be nothing at all and still come back fast enough that a wake isn't an outage. Forking a running machine is 400–750ms on the same host, which is what makes per-branch environments cheap.

The honest trade-offs: Rails here is generic runtime detection plus a start command, not a Rails-aware buildpack with a release phase — you wire the migrate step yourself, the way you would on ECS. It is a younger platform than everything above it on this list, with a correspondingly smaller ecosystem and fewer people who have hit your exact problem before you. And hardware-level isolation per app is worth paying for when you are running many tenants' code or many client apps side by side, and is largely irrelevant when you are running one Rails monolith you wrote yourself. Pick this if per-app kernel isolation, scale-to-zero economics, or cheap environment-per-branch are things you actually need; skip it if a conventional PaaS already fits.

The one-line version

  • Heroku — Best for: the least-surprising Rails deploy in existence, where Procfile and release phase are native concepts. Trade-off: a comfortable, sticky abstraction on a platform evolving more slowly than the ecosystem around it.
  • Render — Best for: Heroku's shape with a more current platform underneath, with workers, cron, and managed Postgres as first-class objects. Trade-off: you live inside their build environment and their machine sizes.
  • Fly.io — Best for: apps where user-perceived latency across regions is a product requirement. Trade-off: multi-region Rails is real distributed-systems work, and the database is closer to self-managed than fully managed.
  • Railway — Best for: fastest path from repo to running app, with excellent per-branch preview environments. Trade-off: less control over the machine, and a usage-based model that rewards knowing your own consumption.
  • Hatchbox / Kamal on your own VPS — Best for: best economics and total control, especially now Rails 8's Solid trio removes Redis from the picture. Trade-off: you own patching, backups, monitoring, and the pager.
  • AWS (ECS / Elastic Beanstalk) — Best for: compliance boundaries, an existing AWS footprint, and RDS-grade database operations. Trade-off: nothing is Rails-shaped by default; expect a platform-engineering project, not a deploy.
  • PandaStack — Best for: per-app kernel isolation, genuine scale-to-zero, and cheap environment-per-branch on a snapshot-restore substrate. Trade-off: generic runtime detection rather than a Rails-aware buildpack, and a younger, smaller ecosystem.

Picking by scenario

  1. Side project or small SaaS, one developer: take the shortest path to a running app with managed Postgres and a worker — Railway or Render — and set Active Storage to object storage before the first user uploads anything. If you enjoy servers and want the bill to stay flat as traffic grows, a single VPS with Kamal is entirely defensible and Rails 8 made it more so.
  2. Funded startup shipping daily: Render or Fly.io, chosen on whether geography matters. Insist on a pre-traffic release hook for migrations, preview environments per branch, and a tested database restore before you have customers. Revisit the decision when the database becomes the constraint, because that is what actually forces the next migration.
  3. Agency deploying many client apps: optimise for repeatability and blast radius, not for any single app's convenience. Either a templated Kamal setup you can stamp out per client, or a platform where each app is genuinely isolated from the others — this is the case where microVM-per-app isolation, PandaStack's included, is a real answer rather than a nice-to-have, because one client's runaway process should not be another client's incident.
  4. Regulated or enterprise: AWS, almost regardless of your technical preferences, because the deciding factors are the audit boundary, the existing agreement, and who signs off. Budget for the platform-engineering work honestly, run migrations as a discrete pipeline task, and put RDS in place from the start rather than migrating to it later under duress.

The short version

For most Rails apps the platform matters less than four decisions: a long-lived Puma process sized against your database's connection limit, a worker process that shares the release, migrations in a pre-traffic step that blocks the deploy on failure, and Active Storage pointed at object storage. Get those right and Heroku, Render, Railway, Fly, Kamal, AWS, and PandaStack all work. Get them wrong and none of them save you — you just get to be surprised in a different vendor's dashboard. If you're leaving Heroku specifically, the migration mechanics are worked through at /blog/best-heroku-alternatives-2026, and the economics of idling apps at /blog/scale-to-zero-app-hosting-explained.

Frequently asked questions

Where should rails db:migrate run during a deploy?

In a release step that runs after the build succeeds and before the new version receives traffic, exactly once. Heroku calls this the release phase, Render calls it a pre-deploy command, Fly calls it a release command, and on ECS or a microVM platform you run it as a discrete task in your pipeline. Do not put it in the container entrypoint or the start command: every instance then runs it on every boot, and while Rails takes an advisory lock that prevents them interleaving, the practical result is replicas blocking on that lock during startup, health checks timing out, and a deploy that appears hung. The other reason matters more — if migrations run at boot, a failing migration turns a blocked deploy into a crash-looping production app.

How many Puma workers and threads should a Rails app run?

Work backwards from two limits: memory and database connections. Each Puma worker is a forked copy of your application, so worker count is bounded by instance memory — measure a worker's resident size under real load rather than trusting a formula, and leave headroom so the OOM killer stays out of it. Thread count is bounded by your database, because Rails opens one connection per thread per process: peak connections are roughly workers × threads × instances, plus your Sidekiq concurrency, and that total has to fit inside the server's max_connections with room for consoles and rake tasks. Also make sure the pool setting in database.yml is at least RAILS_MAX_THREADS, or threads will queue inside your own process while the database sits idle.

Do I still need Redis for a Rails app in 2026?

Often not. Rails 8 ships Solid Queue for background jobs, Solid Cache for caching, and Solid Cable for Action Cable, all backed by your existing database rather than a separate Redis instance. For a large class of applications that removes a whole managed service, its failover story, and its line on the invoice. Redis still wins where you need very high job throughput, very low-latency cache reads at high volume, or the specific Sidekiq features and ecosystem that a decade of production use has built up. The honest framing is that Redis went from a default dependency to a deliberate choice, which is a meaningful simplification when you're comparing hosting platforms — one less thing every platform has to offer you.

Can I host Rails on serverless functions?

You can make it run, and you generally shouldn't. Rails eager-loads a substantial amount of code in production, so cold starts are paid on a framework that was designed to boot once and serve for weeks. There is no long-lived process for Active Job workers or recurring jobs, so background work needs an entirely separate home. And database connections become a real problem, since each concurrent invocation opens its own — a traffic spike exhausts max_connections unless you put a transaction-mode pooler in front, which then requires disabling prepared statements. By the time you've assembled a pooler, a worker platform, and object storage for Active Storage, you have rebuilt most of a PaaS. A long-lived process is the shape Rails was designed for; pick a platform that provides one.

Why do my Rails user uploads disappear after every deploy?

Because Active Storage is still using the local disk service and the platform's filesystem is ephemeral. Each deploy produces a fresh filesystem and anything written to the previous one is gone, with no error at any point — which is why this is usually discovered weeks later as broken images rather than at deploy time. Configure an S3, GCS, or S3-compatible service in config/storage.yml and point the production environment at it, then migrate the files you still have before the next deploy. Precompiled assets are a separate concern and are fine on local disk, because they are regenerated deterministically at build time and served with digested filenames.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.