Step 1: Create the RDS instance
Add anrvn-rds module to the environment your app runs in, in the same network module, with engine: postgres. Match the Heroku Postgres major version (heroku pg:info --app my-app shows it) — you can upgrade after the migration. Size the instance from Heroku’s plan: the standard-0 plan is roughly a db.t4g.medium; standard-2 and up map to db.m7g or db.r7g classes.
ravion.yaml
allowed_security_group_ids instead.
Apply the config. The module generates the master password into Secrets Manager; the endpoint and the secret ARN are in the module’s outputs.
RDS for PostgreSQL 15 and later requires TLS by default (
rds.force_ssl = 1). Make sure your app’s connection string works with sslmode=require before the cutover — see PostgreSQL SSL connection error.Step 2: Reach the database
The RDS instance is in a private subnet, sopg_restore has to run from inside the VPC or over a tunnel. Options, from simplest:
- A pipeline step. Run the dump and restore from a
customstep — it runs on an EC2 instance in your VPC with no extra setup. Installpostgresql-clientin asetupaction, pull the Heroku dump URL and the RDS credentials from Secrets Manager, and run the commands below. - A bastion or SSM tunnel from your laptop to a small EC2 instance in the VPC.
- Temporary public access. Set
public_access_enabled: trueand add your IP toallowed_cidr_blockson the module, apply, migrate, then revert. Fine for a one-off; do not leave it on.
Step 3: Do a dry run
Run the whole procedure once against a scratch RDS instance before the real thing. You want two numbers: how long the dump takes, and how long the restore takes. Their sum is your maintenance window. A restore of a few tens of GB typically takes 10–30 minutes with parallel jobs.Step 4: Migrate with a maintenance window
1
Announce the window and stop writes
Scale worker services to zero and put the app in maintenance mode, or stop the web service. Any write that happens after the dump starts is lost, so make sure nothing is writing.
2
Take a fresh backup on Heroku
pg_dump custom-format archive. Alternatively, get a signed URL with heroku pg:backups:url and curl it from inside the VPC.3
Restore into RDS
PGPASSWORDkeeps the password out of the command line and shell history;~/.pgpassworks too.--no-acl --no-ownerdrops Heroku-specific role grants that do not exist on RDS.--jobsrestores tables in parallel; set it to the number of cores on the machine running the restore.- Expect warnings about extensions Heroku installs by default that you do not use. Errors about
CREATE EXTENSIONfor extensions you do use mean you needrds_superuser(the master user has it) or the extension is not available on RDS.
4
Verify
Compare row counts on a few large tables between Heroku and RDS. Check that sequences are at the right values (
SELECT last_value FROM my_table_id_seq) — a restore sets them correctly, but this is the thing that hurts most when it is wrong. Run any data validation scripts your app has.5
Point the app at RDS
Update the
DATABASE_URL key in your environment secret to the RDS connection string (with ?sslmode=require), then redeploy the web and worker services. Redeploying is required: ECS reads secrets when a task starts.6
Reopen writes
Take the app out of maintenance mode and scale workers back up. Watch error rates and the RDS metrics tab for CPU, connections, and IOPS.
Zero downtime with dual writes
If you cannot take a window, the pattern is:- Create the RDS instance and do an initial dump and restore as above, without stopping writes.
- Change the application to write every mutation to both databases while continuing to read from Heroku. This is application code — an ORM hook or a repository layer — and it has to handle a failed write to one side.
- Backfill rows that changed on Heroku between the initial restore and the moment dual writes started (compare
updated_atcolumns, or re-copy the affected tables).updated_atcannot find rows deleted in that gap — use soft deletes, log deletes to a table you replay, or re-copy the affected tables. If your schema cannot support this, use AWS DMS with change data capture (below) instead. - Validate the two databases match, then switch reads to RDS.
- Stop writing to Heroku.
Afterwards
- Keep the Heroku database for about a week, on the smallest plan, in case you need to recover anything. Then delete it.
- Turn on the module’s CloudWatch alarms and review the RDS metrics after a full day of traffic. Adjust
instance_classandmax_allocated_storageif needed — both are in-place changes. - Consider RDS Proxy if your Heroku setup relied on PgBouncer.