VPS Snaps

How to back up a Railway Postgres database

Railway's database templates are unmanaged, so backups are your job. Turn on a backup schedule on the Postgres service's Backups tab for quick rollbacks, and also run pg_dump on a schedule from a cron service in the same project, over the private network, to storage outside Railway. To recover, load the dump into a new Railway Postgres with pg_restore and check it before you switch the app over.

10 min readUpdated Checked against official documentation

Railway's built-in backups

Railway backs up a service's volume, which for a database template holds the whole data directory. Set a schedule on the database service's Backups tab:

ScheduleRunsKept for
DailyEvery 24 hours6 days
WeeklyEvery 7 days27 days
MonthlyEvery 30 days89 days
  • You can choose several schedules for one volume and take a manual backup at any time. A manual backup is limited to 50% of the volume's size.
  • Railway's pricing page lists built-in database and volume backups on the Pro and Enterprise plans, not Free or Hobby (as of October 2026).
  • Backups are incremental and copy-on-write. You pay only for data unique to each backup, at the volume storage rate: $0.15 per GB per month as of October 2026.

To restore, click Restore on a backup. Railway stages a new volume, named after the backup's date, in place of the old one, which stays in the project unmounted. Review the staged change and click Deploy. Before a risky migration, take a named backup from the CLI:

Terminal
railway postgres pitr backup create --service Postgres --name pre-migration

The same tab can turn on point-in-time recovery for Postgres. It archives WAL with pgBackRest to a Railway storage bucket, keeps the last four weekly full backups (roughly four weeks), and restores to a new service beside the original. It reaches back only to the first base backup taken after you enable it. PostgreSQL point-in-time recovery explains how WAL replay works.

Why you also need your own dumps

  • Same account, same project. Volume backups and the PITR bucket live in the project beside the database. Deleting the project, or losing access to the account, takes them with it.
  • Wiping the volume deletes every backup. Railway lists this among the feature's caveats.
  • They restore only into the same project and environment. You can't load one into another project, another provider or your laptop.
  • Not on every plan. Railway's pricing page doesn't list them on Free or Hobby. There, a dump is the only backup.

A pg_dump file has none of those limits. It loads into any PostgreSQL server of the same major version or newer. Keep it in storage under a different account: the 3-2-1 rule applied to a hosted database, as in backing up a managed database.

Dump from your machine through the TCP proxy

Railway databases are private by default. Open the database service's Settings → Networking and add Public Access. Railway creates a TCP proxy and fills in DATABASE_PUBLIC_URL, a connection string like postgresql://postgres:<password>@<name>.proxy.rlwy.net:<port>/railway. Copy it from the Variables tab and paste it into your shell without echoing it:

Terminal
read -rs DATABASE_PUBLIC_URL && export DATABASE_PUBLIC_URL

pg_dump refuses to dump a server newer than its own major version. Railway's Postgres template deploys PostgreSQL 18 as of October 2026 (ghcr.io/railwayapp-templates/postgres-ssl:18), and older services may run an earlier major. Compare the two:

Terminal
psql "$DATABASE_PUBLIC_URL" -Atc "show server_version"
Terminal
pg_dump --version

Ubuntu 24.04's own client is version 16, which fails against an 18 server. Install postgresql-client-18 from the PostgreSQL apt repository, as shown in the managed database guide. Then dump:

Terminal
PGSSLMODE=require pg_dump "$DATABASE_PUBLIC_URL" -Fc -w -f railway-$(date +%F).dump
  • PGSSLMODE=require refuses an unencrypted connection. Railway's Postgres image generates its own self-signed certificate, so verify-full has no public CA to check against; require encrypts without checking identity.
  • -Fc writes a compressed custom-format archive for pg_restore. -w never prompts for a password, so a script fails at once instead of hanging.

Railway bills traffic through the TCP proxy as network egress, $0.05 per GB as of October 2026. When you're done, remove the proxy again in Settings → Networking, or with railway tcp-proxy delete, so the database has no public endpoint. Formats, compression and selective dumps are in the pg_dump guide.

To skip the public endpoint entirely, the Railway CLI can tunnel over SSH. railway connect Postgres --tunnel-only (use your database service's name) prints a local host, port and connection URL and holds the tunnel open until Ctrl+C. Run pg_dump against that URL in a second terminal.

Schedule dumps inside Railway on the private network

For nightly backups, run pg_dump as a cron service in the same project and environment. It reaches the database at its railway.internal name over the private network, which is encrypted with WireGuard and doesn't count as egress, so the database needs no public endpoint. Put two files in a Git repository:

Dockerfile
FROM postgres:18-alpine
RUN apk add --no-cache aws-cli
COPY backup.sh /usr/local/bin/backup.sh
RUN chmod 755 /usr/local/bin/backup.sh
ENTRYPOINT ["/usr/local/bin/backup.sh"]

The official image is there only for its client tools; this container never starts a server. Use your server's major version as the tag, or a newer one. aws-cli comes from Alpine's community repository.

backup.sh
#!/usr/bin/env bash
set -euo pipefail

STAMP="$(date -u +%Y-%m-%d_%H%M)"
FILE="/tmp/railway-$STAMP.dump"
trap 'rm -f "$FILE"' EXIT

pg_dump "$DATABASE_URL" -Fc -w -f "$FILE"
pg_restore -f /dev/null "$FILE"
aws s3 cp "$FILE" "s3://$S3_BUCKET/postgres/railway-$STAMP.dump" --only-show-errors
echo "Uploaded railway-$STAMP.dump"
  • set -euo pipefail ends the run with a non-zero exit at the first failed command.
  • pg_restore -f /dev/null reads the whole archive back, so a truncated dump fails before it is uploaded.
  • The file goes to ephemeral storage: 1 GB on the Free plan, 100 GB on paid plans. For larger dumps, pipe straight to the bucket with pg_dump "$DATABASE_URL" -Fc -w | aws s3 cp - s3://<bucket>/<key>, and add --expected-size in bytes once a stream can pass 50 GB.

Create a service from the repository in the database's project and environment. Railway builds the Dockerfile it finds at the root. On the new service's Variables tab, add:

Service variables
DATABASE_URL=${{Postgres.DATABASE_URL}}
S3_BUCKET=my-railway-backups
AWS_ACCESS_KEY_ID=<key-id>
AWS_SECRET_ACCESS_KEY=<secret>
AWS_DEFAULT_REGION=us-east-1

${{Postgres.DATABASE_URL}} is a reference variable. It copies the database service's private connection string, which points at its railway.internal name on port 5432. Seal the secret from its menu so the UI never shows it again. For other S3-compatible storage, also set AWS_ENDPOINT_URL to the provider's endpoint, as in the Cloudflare R2 and Backblaze B2 guides. Give the key write access to that one bucket, as in the S3 bucket guide. Then, in the service's Settings, set Cron Schedule:

Cron Schedule
15 3 * * *
  • Railway cron schedules are in UTC, so this runs at 03:15 UTC. Runs must be at least 5 minutes apart and can start a few minutes late.
  • The script must exit. If the previous run is still Active when the next is due, Railway skips the new one, and it doesn't stop a hung run for you. One stuck dump halts every later backup.
  • The default restart policy, On Failure with up to 10 restarts, re-runs a failed dump. Set it to Never if you'd rather a failure stop and alert you.

For alerts, add a Slack or Discord webhook URL under the project's Settings → Webhooks. Railway posts deployment status changes there, including Crashed. A run that never ends sends nothing, so also check that a new file lands in the bucket each day.

What each route costs in egress

pg_dump compresses on the machine that runs it. Through the proxy, table data leaves Railway uncompressed and shrinks only on your machine. Inside Railway, only the compressed file leaves, once, when the cron service uploads it. Say the tables hold 6 GB and the dump compresses to 1.5 GB, taken nightly at $0.05 per GB:

RouteLeaves Railway per runPer 30 daysEgress
pg_dump on your machine, through the proxyAbout 6 GB180 GB$9.00
Cron service, upload to your bucket1.5 GB45 GB$2.25

The cron service also bills CPU and RAM, but only while it runs.

Restore into a new Railway Postgres

Restore into a new database, not over the live one, so you keep the damaged data to compare.

  1. Add a PostgreSQL database with + New on the project canvas. It must run the same major version as the dump's source, or newer. If the original came from a template with extensions such as pgvector or PostGIS, deploy that template: Railway's plain Postgres template doesn't add extensions, so CREATE EXTENSION fails.
  2. Add Public Access to the new database, or open a tunnel to it with railway connect, and load its URL into NEW_DATABASE_URL as before.
  3. Download the dump, then restore it, as shown below.
  4. Check the data, then point the app's DATABASE_URL reference variable at the new service, redeploy the app, and remove Public Access.
Terminal
aws s3 cp s3://my-railway-backups/postgres/railway-2026-10-03_0315.dump .
Terminal
PGSSLMODE=require pg_restore -j 4 -d "$NEW_DATABASE_URL" railway-2026-10-03_0315.dump
  • Railway connects you as postgres, which the official image creates as a superuser, so ownership and grants come back as they were. If you created other roles, create them on the new server first (PostgreSQL roles), or add --no-owner --no-privileges.
  • -j 4 loads data and builds indexes in four parallel sessions. It needs a custom or directory archive read from a file.
  • By default pg_restore continues past errors and prints a count at the end. Read it before you trust the result.

Test the restore every month

  1. Restore the newest dump into a throwaway Postgres as above, adding --exit-on-error so the first error stops it.
  2. Compare row counts on tables that matter against the live database. Rows written after the dump will differ, so count up to the dump's time, for example with where created_at < '2026-10-03 03:15'.
  3. Note how long the download and restore took. That is your restore time, the number behind your RTO.
  4. Delete the throwaway service so it stops billing.
Terminal
psql "$NEW_DATABASE_URL" -Atc "select count(*) from orders where created_at < '2026-10-03 03:15'"

Put one of your own tables in place of orders, run the same query against the live database, and compare. More checks are in testing a backup restore.

MySQL and Redis on Railway

The same routes apply. For MySQL, Public Access fills MYSQL_PUBLIC_URL; inside the project, reference the MySQL service's MYSQLHOST, MYSQLPORT, MYSQLUSER, MYSQLPASSWORD and MYSQLDATABASE and pass them to mysqldump. Railway's point-in-time recovery also covers MySQL, with a 7-day window. For Redis, Public Access fills REDIS_PUBLIC_URL, and redis-cli can pull a snapshot through it:

Terminal
redis-cli -u "$REDIS_PUBLIC_URL" --rdb redis-$(date +%F).rdb

Restoring an RDB file is covered in the Redis backup guide.

Common errors

ProblemFix
aborting because of server version mismatchpg_dump is older than the server. Install the matching postgresql-client package, or raise the image tag in the cron service's Dockerfile.
could not translate host name for a railway.internal namePrivate names resolve only inside the project's environment, at runtime. From outside, use DATABASE_PUBLIC_URL or a railway connect tunnel.
Cron runs are skippedAn earlier run is still Active. Find out why it never exited, then stop it.
CREATE EXTENSION fails during a restoreThe new database's image doesn't ship that extension. Deploy the same template as the original.
The cron run is stopped partway through a large dumpThe file outgrew ephemeral storage. Stream the dump to the bucket instead of writing it to /tmp.

Frequently asked questions

Does Railway back up my Postgres database automatically?
Railway backs up the database's volume on the schedules you pick on its Backups tab: daily, weekly or monthly. Its pricing page lists these backups on Pro and Enterprise. They stay in your Railway project, so keep your own pg_dump copies elsewhere too.
How long does Railway keep backups?
Daily backups are kept for 6 days, weekly for 27 days and monthly for 89 days. Wiping the volume deletes all of them.
Should pg_dump use DATABASE_URL or DATABASE_PUBLIC_URL?
DATABASE_URL from a service in the same project and environment: it uses the private network and costs no egress. DATABASE_PUBLIC_URL from anywhere else: it goes through the TCP proxy, needs Public Access on, and is billed as egress.
Can I restore a Railway backup into another project?
No. Railway's backups restore only into the same project and environment. To move a database elsewhere, restore a pg_dump file with pg_restore.

How this was checked

Commands, limits and prices were checked against these official pages, on October 3, 2026: