Infra Stack Review

Migrating a Rails App from Heroku to AWS Without Downtime

A practical blueprint for moving Rails apps to AWS with minimal operational risk.

· 12 min read · Updated
Cover illustration for “Migrating a Rails App from Heroku to AWS Without Downtime”
PaaS Migration Guides · August 14, 2026 · 12 min read · 2,627 words

Moving a Rails app off a shared platform and onto AWS without downtime is a solvable problem. It follows a repeatable sequence: map every service the app depends on, pick a compute path, migrate the database with a method sized to the data, and run a cutover that's been rehearsed before it counts.

Why migrating a Rails app away from a shared PaaS is now common

Small engineering teams, with no dedicated DevOps hire and no infrastructure specialist on staff, can now plan and run this migration themselves. The financial and operational case has become specific enough to act on, rather than a vague someday project.

Billing tiers set the clock. Teams running on lower compute tiers have one to two quarters before the move needs to happen. Teams already paying for higher tiers face a case for moving now, because the premium they're paying buys nothing further up the ladder. That's a real deadline, tied to a real bill, not a hunch about where the industry is headed.

Cost alone doesn't carry the argument, and it shouldn't. Most migration guides oversell the savings. The compute cost really does drop once a workload moves to AWS, but NAT gateway charges, data egress fees, and the engineering hours spent building and testing the move eat into that gap, often by a lot, on a small app. The honest reason to migrate is control: owning the network, owning the compliance posture, and not depending on a platform that may deprioritize a given workload over time. Add-on integrations built by third parties tend to get less maintenance attention as a shared platform's growth slows, so the risk of staying put rises even when the monthly bill doesn't move. Teams still running on an older generation of the stack also have a new tooling wrinkle: build tooling that supports that generation only started working recently, changing what's available for anyone who hasn't already modernized.

None of this starts with picking a server or writing Terraform. It starts with an inventory. A Rails app running on a shared platform isn't one thing, it's a bundle: compute, a database, a cache layer, background workers, file storage, and logging, each wired up through the platform's own conventions. Every one of those pieces needs a named AWS equivalent before a migration plan means anything. Skipping this step is the single most common cause of unplanned downtime on cutover night, and it's almost never a technical failure when it happens. It's a missed line item: a config var nobody wrote down, an add-on nobody mapped to an AWS service.

The fix is a two-to-three-day sprint, done before a single line of migration code gets written. Every service gets inventoried, every add-on gets catalogued, with its AWS target named next to it. Every config var gets recorded. Teams that do this work upfront tend to finish the entire migration weeks faster than teams that start building first and documenting as they go.

One more rule belongs at the start of that sprint: freeze schema changes. No ALTER TABLE, no CREATE INDEX, nothing that changes the shape of the database, from the first day of documentation until cutover is done. A data migration plan built against a moving schema is a plan built against a target that won't hold still, and that's how timelines slip.

Choosing a compute path: EC2 vs. ECS Fargate

Compute decisions come before infrastructure code gets written, because the choice shapes everything downstream: network design, the CI/CD pipeline, how scaling works, and how incidents get handled at 2 a.m.

Three paths show up in practice when teams choose a compute path, and none of them is the objectively correct one. The right answer depends on what the team can actually operate.

The first is a lift-and-shift to EC2. Instances get provisioned to match the current process configuration, and whatever ran as a Procfile process type becomes a systemd service or a Docker container, sitting behind an Application Load Balancer. This is the fastest path to a working app, sometimes a matter of days for a simple application. It's also the path with the least abstraction and the most ongoing manual work: patching the operating system, sizing instances, and scaling all fall on the team unless they build automation for it separately.

The second path, and the one most teams land on, is ECS Fargate. The app gets containerized, task definitions get written, and the team builds its own VPC, load balancer, and security groups before deploying through an ECS service. Fargate costs less than EC2 for workloads that vary in traffic, and it removes operating-system maintenance from the team's plate. But the team now owns VPC design, load balancer configuration, and task definition management directly, which is the hidden tax that a shared platform used to absorb quietly. A single NAT gateway carries a meaningful fixed monthly charge before a single byte of traffic moves through it, and a standard two-availability-zone private subnet design needs two of them running. Combined, that fixed cost can run larger than the entire compute savings a small app was expecting to see.

The third path is a managed container platform that deploys straight into the team's own AWS account, handling VPC setup, cluster management, and CI/CD wiring on the team's behalf. This option keeps most of the simplicity teams liked about a shared platform while keeping the workload inside the team's own cloud account. It suits teams who want to own their infrastructure without building the Fargate wiring by hand. The trade is a different kind of platform dependency in exchange for a lighter ops load.

Picking among these three comes down to what the team can run and debug, not which option sounds most advanced on a slide. A clean EC2 lift-and-shift that the team fully understands beats a Fargate cluster nobody can troubleshoot at 2 a.m.

Moving the database: the step that determines whether the migration has downtime

Diagram: Database Migration Method by Data Size and Downtime Budget. Visualizes: Visualize the three-tier decision ladder for choosing a Rails database migration method, based on data size and downtime tolerance.

Every other part of this migration can be rehearsed, paused, or rolled back more cleanly than a database caught half-migrated. The database cutover is the step where the whole plan either holds or falls apart, and it deserves to be treated that way.

What to use depends on how much data there is and how tight the downtime budget is, not on which method sounds most sophisticated in a planning document. Under 50 GB, a plain pg_dump and restore, run inside a short maintenance window, is the simplest option and the most reliable one. There's little reason to build anything fancier at that size.

Between 10 and 100 GB, with a requirement for close to zero downtime, AWS Database Migration Service can work, but only running in change data capture mode. DMS running in full-load mode carries downtime roughly on par with a plain pg_dump, so it should never get planned as if it were a zero-downtime solution. Teams that treat full-load DMS as a CDC substitute find that out the hard way, usually during the maintenance window itself.

At 100 GB and above, the realistic choices are a planned maintenance window or logical replication. Trying to avoid any downtime at this scale without logical replication adds a layer of complexity that tends to backfire rather than pay off.

Two documented paths get teams to near-zero downtime beyond DMS. One, used by Checkly, restores a write-ahead log into an intermediate Postgres instance, then runs logical replication from that instance into RDS. The other, used by One2N, relies on trigger-based replication through Bucardo. Both require real setup work, and both buy a cutover window measured in minutes instead of hours.

One design choice narrows the gap between those extremes: migrating immutable data, historical partitioned tables being the clearest example, before the maintenance window even opens. Whatever gets moved ahead of time doesn't need to be synchronized during the window itself, so the window only has to cover what's still changing. Checkly specifically pointed to this decision as the reason its cutover ran as efficiently as it did.

Regulated workloads add one more constraint on top of all this. AWS lists ECS, EKS, Fargate, Elastic Beanstalk, Amplify, and Lambda as HIPAA-eligible services. App Runner and Lightsail are not on that list. Any database path for a health or fintech workload needs to route data through the eligible services and keep it on private network paths, which rules out a few otherwise-convenient shortcuts before the technical planning even starts.

The zero-downtime cutover sequence, step by step

Zero downtime is a sequence of preparation steps, done in advance, that shrink the actual maintenance window down to a few minutes by handling every foreseeable risk before it becomes one.

The sequence starts well before cutover night. The service map and schema freeze from the documentation sprint need to already be in place, since nothing after this point works without them. Immutable and historical data partitions get pre-migrated into RDS during this window too, which is what shrinks the dataset that still has to move during the real cutover. The full AWS environment gets stood up and run in parallel against a read replica or a test dataset, so the app is proven out on the new infrastructure before it carries real traffic. Then the entire cutover gets executed end to end, DNS flip and database promotion included, three separate times against a snapshot of production data, with every step timed and every surprise written down. Checkly ran this rehearsal three times and still turned up a surprise on the second attempt, which says something about how much even careful planning benefits from repetition.

DNS handles the actual traffic switch. At cutover, DNS records get updated to point at the new AWS environment's load balancer endpoint. The old environment stays running and keeps accepting traffic until DNS propagation is confirmed across the board. Tearing down the old environment on cutover night, before that confirmation lands, is the kind of move that turns a minor hiccup into an outage.

The maintenance window itself is short by design, because almost everything risky has already happened during rehearsal. The app goes into maintenance mode, background job queues get flushed, and a final database snapshot gets taken. The RDS instance gets promoted, or replication gets finalized, pending migrations run against RDS, and DNS gets flipped. Checkly had planned for a 30-minute window and used only 10, a direct result of the three dry runs and the pre-migrated partitions done ahead of time.

The work isn't done once DNS flips. Error rates, latency, and background job throughput need watching for a full day or more before anyone decommissions the old environment. The old environment stays on standby as a rollback option during that period, and the criteria for triggering a rollback need to be written down in advance, not debated in the middle of a live incident.

Rebuilding CI/CD and infrastructure-as-code on AWS

Teams underestimate the CI/CD rebuild more often than any other part of this migration, and that mistake becomes costly fast. Skipping the budget for it leaves the team hand-running fragile deployments that recreate the exact operational mess the migration was supposed to fix.

A pipeline pattern that shows up often in practice uses GitHub Actions paired with OIDC federation into AWS, so no long-lived access keys sit in GitHub Secrets waiting to leak. The pipeline runs through clear stages: build, test, a security check, push the image to ECR, then deploy to ECS with rolling updates that don't interrupt traffic. Removing stored credentials this way is a security improvement on its own, and it also helps satisfy compliance controls under frameworks like SOC 2. Once this pipeline exists, blue/green and canary deployments become available as options, neither of which was possible on a shared platform without bolting on extra tooling.

Infrastructure-as-code deserves the same early investment. The full stack, the VPC, subnets, security groups, ECS cluster, RDS instance, ElastiCache cluster, load balancer, and auto-scaling configuration, gets defined in Terraform or CloudFormation rather than clicked together by hand. Building this takes real time upfront, and that cost is honest: writing infrastructure as code is not a quick task. It pays for itself the first time something breaks at 2 a.m., or the first time the team needs to spin up a second environment, because a reproducible environment is also a recoverable one. Version-controlled infrastructure also makes collecting SOC 2 Type II evidence considerably easier, particularly for change management controls, even though it isn't a strict requirement for passing an audit.

What AWS ownership means for compliance

Everything built so far pays off here. Owning the AWS account unlocks compliance work that a shared, multi-tenant platform simply can't support, and that matters directly for any startup selling into healthcare, finance, or another regulated space.

HIPAA eligibility is a design constraint, not an afterthought. ECS, EKS, Fargate, Elastic Beanstalk, Amplify, and Lambda are all HIPAA-eligible. App Runner and Lightsail are not on that list. A health or fintech Rails app has to route its workload through the eligible set, full stop, which shapes the compute decision made earlier in this process.

Owning the VPC puts network segmentation, private subnets, and security group rules directly under the team's control, something a shared platform structurally cannot hand over. Without that control, compliance evidence is harder to produce and harder to defend when an auditor starts asking questions. The CI/CD pipeline from the previous section turns out to double as a compliance artifact: OIDC-based access, artifacts promoted the same way across every environment, and infrastructure defined in code together generate the access logs and change records that SOC 2 Type II asks for.

A managed platform that deploys into the team's own AWS account offers a middle path here. It can automate infrastructure-level controls, VPC configuration, encryption at rest, patching known vulnerabilities, while still leaving the team inside their own cloud account for signing a compliance agreement with a vendor and standing up to an audit. That combination lets a small team move faster on compliance without giving up ownership of the account the auditor actually cares about.

What the documented case studies show about timelines, costs, and what went wrong

The pattern across documented migrations is consistent: preparation buys downtime measured in minutes, and skipping it costs hours or a rollback. Teams that ran dry runs, froze their schema, and pre-migrated immutable data landed close to zero downtime. Teams that skipped those steps paid for it, either in a maintenance window that ran long or in a rollback nobody had planned for in advance.

Checkly's case makes the pattern concrete. Three full rehearsals of the cutover sequence, run against production snapshots, still turned up a surprise on the second attempt, showing that rehearsal finds problems a plan on paper never will. Pre-migrating historical partitioned data ahead of the maintenance window shrank what had to move during the real cutover down to a size that could be handled in minutes. The planned window was 30 minutes. The real one took 10.

The honest accounting matters as much as the success stories. The compute cost on AWS is lower, that part of the pitch is true. But NAT gateway fees, egress charges, and the engineering hours spent mapping services, building pipelines, and rehearsing cutverts narrow that gap substantially for a small app, sometimes closing it. The case for migrating holds up on different grounds: direct control over the network, a compliance posture that a shared platform can't offer, and freedom from add-on integrations that lose maintenance priority as the platform they sit on slows its own growth. Teams that go into this migration expecting a clean cost win may come out disappointed. Teams that go in expecting control, and a system they understand end to end, get what the documented cases delivered.

More in PaaS Migration Guides