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.
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:
| Schedule | Runs | Kept for |
|---|---|---|
| Daily | Every 24 hours | 6 days |
| Weekly | Every 7 days | 27 days |
| Monthly | Every 30 days | 89 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:
railway postgres pitr backup create --service Postgres --name pre-migrationThe 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:
read -rs DATABASE_PUBLIC_URL && export DATABASE_PUBLIC_URLpg_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:
psql "$DATABASE_PUBLIC_URL" -Atc "show server_version"pg_dump --versionUbuntu 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:
PGSSLMODE=require pg_dump "$DATABASE_PUBLIC_URL" -Fc -w -f railway-$(date +%F).dumpPGSSLMODE=requirerefuses an unencrypted connection. Railway's Postgres image generates its own self-signed certificate, soverify-fullhas no public CA to check against;requireencrypts without checking identity.-Fcwrites a compressed custom-format archive forpg_restore.-wnever 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:
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.
#!/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 pipefailends the run with a non-zero exit at the first failed command.pg_restore -f /dev/nullreads 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-sizein 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:
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:
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
Activewhen 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:
| Route | Leaves Railway per run | Per 30 days | Egress |
|---|---|---|---|
| pg_dump on your machine, through the proxy | About 6 GB | 180 GB | $9.00 |
| Cron service, upload to your bucket | 1.5 GB | 45 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.
- 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 EXTENSIONfails. - Add Public Access to the new database, or open a tunnel to it with
railway connect, and load its URL intoNEW_DATABASE_URLas before. - Download the dump, then restore it, as shown below.
- Check the data, then point the app's
DATABASE_URLreference variable at the new service, redeploy the app, and remove Public Access.
aws s3 cp s3://my-railway-backups/postgres/railway-2026-10-03_0315.dump .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 4loads 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
- Restore the newest dump into a throwaway Postgres as above, adding
--exit-on-errorso the first error stops it. - 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'. - Note how long the download and restore took. That is your restore time, the number behind your RTO.
- Delete the throwaway service so it stops billing.
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:
redis-cli -u "$REDIS_PUBLIC_URL" --rdb redis-$(date +%F).rdbRestoring an RDB file is covered in the Redis backup guide.
Common errors
| Problem | Fix |
|---|---|
aborting because of server version mismatch | pg_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 name | Private names resolve only inside the project's environment, at runtime. From outside, use DATABASE_PUBLIC_URL or a railway connect tunnel. |
| Cron runs are skipped | An earlier run is still Active. Find out why it never exited, then stop it. |
CREATE EXTENSION fails during a restore | The 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 dump | The 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:
- Railway docs: Backups
- Railway docs: Point-in-Time Recovery
- Railway docs: PostgreSQL
- Railway docs: Databases
- Railway docs: MySQL
- Railway docs: Redis
- Railway docs: Cron Jobs
- Railway docs: TCP Proxy
- Railway docs: How Private Networking Works
- Railway docs: Variables
- Railway docs: Services (ephemeral storage)
- Railway docs: Dockerfiles
- Railway docs: Restart Policy
- Railway docs: Set Up Alerts for Crashes, Restarts, and Failed Deploys
- Railway docs: railway connect
- Railway docs: railway postgres
- Railway docs: railway tcp-proxy
- Railway docs: Pricing Plans
- Railway docs: Understanding your bill
- Railway: Pricing (plan comparison)
- Railway: Postgres template
- Railway: postgres-ssl image README
- Docker Official Images: postgres
- Alpine Linux packages: aws-cli (v3.24 community)
- AWS CLI Command Reference: s3 cp
- AWS CLI User Guide: Environment variables
- PostgreSQL documentation: pg_dump
- PostgreSQL documentation: pg_restore
- PostgreSQL documentation: Environment Variables (libpq)
- Redis documentation: redis-cli