← Blog

Move from Railway to your own server

Railyard

May 5, 2026 · 7 min read

Follow this on your own server. Start free, no card needed

Railway is convenient right up until the invoice is bigger than expected, or you want a server you can SSH into instead of a dashboard between you and the machine. Moving off it means two separate jobs: getting your application’s code and configuration running somewhere else, and getting the data out of Railway’s managed Postgres without losing anything or taking the app down for longer than a DNS change needs. Neither is specific to Railway once you see the shape of it: pull the config, dump the database, stand up the same thing on a server you control, then switch DNS.

This guide does that by hand first, with your own VPS and Docker Compose, then covers the one-click import that reads a Railway project directly and does most of this automatically.

What you’ll need

  • A VPS. A Hetzner CX23 (2 vCPU, 4 GB RAM, currently €5.99/month excluding VAT) or a DigitalOcean 2 GB droplet ($12/month) is enough for most small apps plus their Postgres database.
  • A Railway account with an API token, and CLI access (npm install -g @railway/cli, then railway login).
  • Your domain’s DNS, ready to repoint once the new server answers.
  • pg_dump and pg_restore installed locally, or run them from inside a container (both ship with the official postgres Docker image).

Step 1: find out what’s actually running

Before exporting anything, link the CLI to the project so you can see its services and the exact variables each one has:

railway login
railway link
railway variable list

That prints every variable Railway has set for the linked service, including ones it generates itself like DATABASE_URL, RAILWAY_PRIVATE_DOMAIN and anything you added yourself. Copy this output somewhere, since it’s the fastest way to get every API key, feature flag and secret in one place without retyping them from the dashboard.

Step 2: give the database a public address

Railway’s Postgres plugin only listens on Railway’s private network by default; DATABASE_URL points at a .railway.internal hostname that your own machine can’t reach. To dump it from outside Railway, open the Postgres service’s Settings → Networking and turn on Public Networking, which creates a TCP proxy and a new DATABASE_PUBLIC_URL variable alongside the private one. List variables again to grab it:

railway variable list

Railway bills network egress for traffic through this proxy, so this is a one-time cost for the migration, not something to leave switched on afterward unless you need it for something else.

Step 3: dump the database

With DATABASE_PUBLIC_URL in hand, dump it from your own machine (or from the new server, which avoids pulling the data twice):

pg_dump "$DATABASE_PUBLIC_URL" --format=custom --file=railway-export.dump

The custom format (-Fc) compresses the dump and lets pg_restore parallelize the restore later, which matters once the database is more than a few hundred megabytes. Keep Railway running while you do this; nothing about a dump touches the live database, so there’s no downtime yet.

Step 4: set up the new server

Install Docker on the VPS the same way regardless of provider:

curl -fsSL https://get.docker.com | sh
apt-get install -y docker-compose-plugin

Write a docker-compose.yml with Postgres, your app, and Caddy for HTTPS:

services:
  db:
    image: postgres:18
    restart: unless-stopped
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

  app:
    build: .
    restart: unless-stopped
    env_file: .env
    depends_on:
      - db
    expose:
      - "3000"

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - app

volumes:
  postgres_data:
  caddy_data:
  caddy_config:

If Railway was building your app from a Dockerfile already, reuse it as-is; the build step doesn’t know or care which platform runs the resulting image. If Railway was building it without one (through its own Railpack/Nixpacks detection), you’ll need to write one now. The exact contents depend on your language, but the pattern is the same one covered in Deploy from a repository: install dependencies, build, and run the start command your app already uses in development.

Step 5: restore the database

Bring up just the database first, then restore into it:

docker compose up -d db
docker compose exec -T db pg_restore --no-owner --no-acl -U myapp -d myapp < railway-export.dump

--no-owner and --no-acl skip Railway-specific role and permission statements that don’t exist on this server, since the dump was made from a database owned by a different role than the one you just created.

Step 6: carry over the environment variables

Translate the railway variable list output from step 1 into a .env file for the app service. Most of it copies over unchanged; a few values need to point at the new setup instead of Railway’s:

DATABASE_URL=postgresql://myapp:changeme@db:5432/myapp
REDIS_URL=redis://redis:6379
# everything else from `railway variable list` that your app reads directly

If the app used Railway’s Redis or another add-on, add it as its own service in docker-compose.yml the same way db is defined, and point the corresponding URL at that service’s name instead of a Railway hostname.

Step 7: write the Caddyfile and go live

myapp.example.com {
	reverse_proxy app:3000
}

Point the domain’s A record at the new server, wait for it to resolve, then start everything:

docker compose up -d --build
docker compose logs -f caddy

You should see Caddy obtain a certificate and the app answer at https://myapp.example.com. Compare a few pages or API responses against the still-running Railway deployment before you consider this done.

Step 8: cut over and decommission Railway

Once the new server has been serving real traffic correctly for a day or two, point any remaining references at it (webhooks, mobile app API URLs, anything hardcoded), and shut down the Railway project. Keep the dump file around for a few weeks before deleting it.

The shortcut: import from Railway directly

Railyard’s Applications → Import reads Railway projects through an API token and builds a plan before anything is created, rather than guessing. From how the import actually works: it reads every project’s services, environments and variables through Railway’s own API, and for each service it carries over the Git repository or the exact image Railway runs, its start command when one is set, its custom domains, and its variables, cleaned of Railway-specific ones. It also detects which variables point at a Postgres, Redis, MongoDB or MySQL add-on from the shape of their URLs, and offers to provision the matching database on the new server and copy the data into it automatically, the same pg_dump-into-a-fresh-database approach as above, without you touching a terminal.

A database with no public address still needs that same Public Networking toggle turned on in Railway first. Nothing can stream a database’s contents without the TCP proxy publishing it somewhere reachable.

Start the import from the migration page, paste a Railway API token, review the plan Railyard shows for each service, and pick the server to deploy it on. See Import from another platform for what carries over automatically versus what still needs a value typed in by hand (Railway’s API doesn’t expose secret file contents, for example), and Hosting costs if the whole reason for this move is to compare what you’re actually paying against a server you provision yourself.

Troubleshooting

pg_dump fails to connect, or hangs. Confirm Public Networking is actually turned on for the Postgres service and that you copied DATABASE_PUBLIC_URL, not DATABASE_URL; the private one resolves to a .railway.internal hostname your machine can’t reach at all, which usually shows up as a DNS resolution failure rather than a connection refused.

pg_restore errors about roles or permissions that don’t exist. Add --no-owner --no-acl to the restore command, which drops ownership and grant statements tied to Railway’s internal role names.

The app can’t reach the database after cutover. Check that DATABASE_URL in the new .env points at the Compose service name (db), not localhost or Railway’s old hostname; inside Docker Compose, services reach each other by service name on the shared network, not by 127.0.0.1.

HTTPS never issues on the new server. The domain’s DNS is almost certainly still pointing at Railway, or hasn’t propagated yet. Check with dig myapp.example.com and compare against the new server’s IP before troubleshooting Caddy itself.

Out of memory while restoring a large dump on a small VPS. pg_restore with the default settings can use a surprising amount of memory on a dump with many indexes. Restore without --jobs (serial, slower, lower memory) on anything under 4 GB of RAM, or size up the server temporarily for the restore and scale back down after.

FAQ

Does this work if I’m not using Postgres? The same shape applies to MySQL (mysqldump/mysql) and Redis (redis-cli --rdb), though the exact commands differ. Railyard’s import also detects Redis, MongoDB and MySQL add-ons the same way it detects Postgres.

Can I migrate gradually, one service at a time? Yes. Nothing requires moving an entire Railway project at once; import or rebuild one service, verify it, then move the next.

What happens to Railway’s ephemeral storage or volumes? Anything written to a Railway volume needs to be copied over separately, usually with scp or rsync against a one-off shell session on the Railway side before you decommission it, since neither the CLI’s variable export nor a database dump touches filesystem volumes.

Is the price difference actually worth the migration? That depends entirely on the app’s size and traffic. Hosting costs and the Heroku bill checker are built to make that comparison concrete instead of a guess, even though the second one is named for Heroku specifically.

The parts that make this feel risky, mainly the database, are also the most mechanical once you’ve done it once: dump, restore, verify, switch DNS. Everything else is just running the same container somewhere that isn’t Railway.

Why people pick Railyard for this

  • Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
  • One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
  • One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
  • Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
  • Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.

Import from Railway directly

Paste a Railway API token and Railyard reads your services, variables and databases for you.

Free plan, no card, and 14 days of Pro for every new team. One flat price per team.