Skip to main content
HostwaresHostwares

Migrating from Railway to Hostwares: A Complete Guide (2026)

Hostwares Team··11 min read

Before you start: when you should stay on Railway

Railway is a well-built platform and its billing model is genuinely different from ours rather than simply worse. Metered per-second usage beats a fixed monthly pack in several real situations, so check these first:

  • Your services are idle most of the time. Railway meters consumption per second. A worker that runs for ten minutes a day costs almost nothing there and costs a full month's pack here. Fixed pricing only wins on workloads that are actually running.
  • You have a dozen services talking over the private network. Railway's internal networking between services in a project, with no egress charge between them, is a real feature and unpicking it is the hardest part of this migration.
  • You need a very large single service. Railway publishes limits of up to 48 vCPU / 48 GB per service on Hobby and up to 1,000 vCPU / 1 TB per service on Pro. Our largest pack is Business at 4 vCPU / 8 GB. If you need one enormous box, we do not have it.
  • You are on Hobby at $5/mo and the $5 usage credit covers you. You are paying five dollars. There is no meaningful saving to chase, and you would give up the project canvas and the templates for it.

The case for moving is a steady-state application, running continuously, where metered usage has stopped being cheaper than a flat rate and has started being an unpredictable line item. That is where this guide applies.

Pricing verified 13 August 2026 from railway.com/pricing. Everything quoted below comes from that page.

What Railway publishes

PlanFeeIncluded usage creditPer-service ceilingLog history
Free Trialno card required$5 in credits for 30 daysUp to 2 vCPU / 1 GB7-day
Free$0/month$1 of monthly usage creditsUp to 1 vCPU / 0.5 GB3-day
Hobby$5/month$5 of monthly usage creditsUp to 48 vCPU / 48 GB7-day
Pro$20/month per workspace$20 of monthly usage creditsUp to 1,000 vCPU / 1 TB30-day

Seats on Pro are "unlimited and included", because the fee is per workspace rather than per seat. That is a better deal than most competitors and worth saying out loud.

The unit rates, quoted exactly: CPU $0.00000772 per vCPU / second, Memory $0.00000386 per GB / second, Volumes $0.00000006 per GB / second, Egress $0.05 per GB for services, Object Storage $0.015 per GB-month with free egress.

Per-second figures are hard to reason about, so here is the arithmetic, which is ours and not a published Railway figure. A 30-day month is 2,592,000 seconds:

  • 1 vCPU, fully used, for a month: 2,592,000 × $0.00000772 = $20.01
  • 1 GB of memory for a month: 2,592,000 × $0.00000386 = $10.01
  • 1 GB of volume for a month: 2,592,000 × $0.00000006 = $0.16

Read those as ceilings, not as your bill. The rates meter consumption, so an idle service costs a fraction of the saturated number, which is exactly why an idle-heavy workload should stay on Railway and a busy one should not. Put your own service list into the Railway cost calculator before doing any of the work below.

Step 1 — Inventory the project

A Railway project is a graph of services, not a single app, and the graph is what you are actually migrating. Write down, for every service: its source repo and root directory, its start command, its variables, whether it has a volume, whether it has a public domain, and whether it has a cron schedule.

npm i -g @railway/cli
railway login
railway link
railway status
railway service

The mapping is one Railway service to one deployment slot, and one slot is one resource pack. Four services becomes four packs. If three of them are near-idle workers, that arithmetic is the whole decision, and it may tell you not to migrate.

Monorepos need care. If several services point at the same repository with different Root Directory settings, each still becomes its own deployment from that repo with its own build and start command.

Step 2 — Export the variables, then flatten the references

railway variables            # table view
railway variables --json     # machine-readable
railway variables --kv       # KEY=value pairs

Flag support varies between CLI versions, so run railway variables --help and use whichever your version has. Then fix three categories before importing anything:

  • Reference variables do not survive the move. Railway lets you write ${{Postgres.DATABASE_URL}} or ${{shared.API_KEY}} and resolves it at deploy time. Off Railway that is a literal string, and it will be passed to your app verbatim. Every reference has to be flattened to the value it currently resolves to. This is the single most common cause of a migrated app that starts and then immediately fails to reach its database.
  • Drop the injected RAILWAY_* variables. RAILWAY_ENVIRONMENT, RAILWAY_PROJECT_ID, RAILWAY_PRIVATE_DOMAIN, RAILWAY_PUBLIC_DOMAIN and the rest are platform-supplied. If your code branches on any of them, that is a code change.
  • Keep PORT behaviour. Railway injects PORT and expects your app to bind it. We do the same, so leave process.env.PORT handling exactly as it is, and keep binding 0.0.0.0 rather than 127.0.0.1.

Step 3 — Move the Postgres data

The trap here is which connection string you use. Railway gives a Postgres service two: DATABASE_URL, which resolves only on the private network inside Railway, and DATABASE_PUBLIC_URL, which goes through the TCP proxy. From your laptop you need the public one, or you need to run the dump inside Railway.

railway variables --service Postgres | grep DATABASE_PUBLIC_URL

Check your client version before you start. pg_dump must be the same major version as the server or newer, otherwise it refuses outright:

psql "$DATABASE_PUBLIC_URL" -c "select version();"
pg_dump --version

Then dump in custom format and restore:

pg_dump -Fc --no-owner --no-acl --no-comments -v \
  -d "$DATABASE_PUBLIC_URL" -f railway.dump

pg_restore --verbose --clean --if-exists --no-owner --no-acl \
  -d "$NEW_DATABASE_URL" railway.dump

Why each flag matters. -Fc gives a custom-format archive, which pg_restore can restore selectively and in parallel, where a plain SQL dump can only go through psql. --no-owner --no-acl strips ownership and grant statements referencing Railway's role names, which do not exist on the target; without them the restore throws on every ALTER OWNER and GRANT. --no-comments avoids the COMMENT ON EXTENSION failures you hit as a non-superuser. --clean --if-exists makes the restore repeatable, and you will run it more than once.

Check extensions too. If your schema uses pgvector, postgis or similar, confirm it is installed on the target first, because CREATE EXTENSION in the dump fails if the binary is not present.

Verify rather than assume. A restore that silently skipped a table is worse than one that failed loudly:

psql "$NEW_DATABASE_URL" -c "\dt"
psql "$NEW_DATABASE_URL" -c "select count(*) from your_busiest_table;"

Compare against the same query on Railway. Managed PostgreSQL here starts at $3/mo with automated backups, as do MySQL, MariaDB, Redis and MongoDB.

Step 4 — Get the volume data out

Volumes are the least convenient part of a Railway migration, because there is no download button. The data lives on a mount inside the service and you have to stream it out through the service itself. With a shell into the container:

railway ssh
# inside the container, /data is wherever your volume is mounted
tar czf - -C /data . | base64

Or, in one line, redirecting the stream to your machine:

railway ssh -- tar czf - -C /data . > volume.tar.gz

Check railway ssh --help first, since the command set has changed across CLI versions. If your version does not support it, the fallback is a temporary one-off command on the service that uploads the archive to an S3-compatible bucket. Either way, do this while the service is quiet, and verify the archive extracts and the file counts match before decommissioning anything.

Worth asking before you do the work: does that volume hold anything you need? Many hold a cache, a search index or uploaded files that belong in object storage anyway, and a migration is a good moment to stop having a stateful container at all.

Step 5 — Replace the private network

Inside a Railway project, services reach each other on <service>.railway.internal over a private network, and that network is IPv6. Two consequences:

  • Every internal hostname in your config is about to stop resolving. Grep your codebase and your variables for .railway.internal and replace each one with the connection details of the new equivalent: a managed database endpoint, or the hostname of the other deployment.
  • If you previously had to make a service listen on :: to be reachable on Railway's IPv6 private network, that requirement goes away. Bind 0.0.0.0:$PORT and move on.

Cost-wise this is one of the few places Railway is straightforwardly better: traffic between services inside a project does not attract the $0.05 per GB egress charge. If you have two services exchanging a lot of data, model that before you split them across separate deployments.

Step 6 — Workers and cron services

Railway models both as ordinary services, which makes them easy to overlook in an inventory. Map them like this:

On RailwayHere
Service with no public domain (a worker)Its own deployment on its own pack, no domain attached
Service with a cron scheduleA scheduled task hitting an endpoint, or a small always-on deployment running its own scheduler
Replicas on a serviceOne pack per replica, no autoscaling between them
Healthcheck path in railway.jsonConfigured on the deployment

Be honest about the cron case. A job running thirty seconds an hour costs almost nothing under per-second billing and costs a full pack here if you give it a dedicated always-on container. The sensible pattern is a scheduled request into an endpoint on an application you already pay for, protected by a shared secret header, rather than a container that exists to sleep.

Step 7 — Deploy, then cut over DNS

Connect the same GitHub repository. Framework detection reads the repo and fills in the build and start commands, so for most services there is nothing to configure beyond pasting the flattened variables. Each deployment gets a working subdomain with SSL, so you can test properly while Railway still serves production.

Test the paths that fail quietly: anything using a .railway.internal hostname, anything reading a RAILWAY_* variable, anything writing to the old volume path, background jobs, outbound webhooks, and any third party with an IP allowlist, since a new host means a new outbound IP. The AI DevOps agent reads build and runtime logs directly, so a failed deploy is usually one question rather than an afternoon.

The day before cutover: lower the TTL on the DNS records pointing at Railway to 300 seconds. This only helps if you do it in advance, and it is what decides whether the switch takes five minutes or most of a day.

At cutover:

  1. Add the custom domain to the new deployment and confirm the certificate has issued and the real hostname serves over HTTPS.
  2. Take a final database sync, repeating step 3, once writes to the old system have stopped.
  3. Replace the CNAME that points at Railway's domain target with the record for the new deployment.
  4. Watch both sets of logs. Traffic drains over roughly one TTL.
  5. Leave the Railway project running for a week. It is your only rollback and reverting is one DNS change.

If you cannot tolerate split writes during the sync, take a short maintenance window during a quiet hour. Anyone promising a zero-downtime cutover on a write-heavy database with no dual-write and no maintenance window is skipping the hard part.

Where Railway stays better

The project canvas is the clearest visual model of a multi-service application anyone ships, and we do not have an equivalent. Per-second metered billing genuinely wins on bursty and idle workloads. Unlimited included seats on Pro at $20 per workspace is better than per-seat pricing for a team of any size. Private networking between many services, with no inter-service egress charge, is a real architectural advantage. Their per-service ceilings, up to 1,000 vCPU and 1 TB on Pro, are far above anything we offer. Railway's templates get a full stack running in a click.

What you get by moving is a fixed number. Micro $2/mo (0.25 vCPU, 256 MB), Starter $3/mo (0.5 vCPU, 512 MB, 10 GB), Standard $7/mo (1 vCPU, 2 GB, 25 GB), Performance $18/mo (2 vCPU, 4 GB, 50 GB), Business $35/mo (4 vCPU, 8 GB, 100 GB). One pack is one deployment slot, and the bill is the same in a quiet month and a busy one.

Does the arithmetic work for you?

Take the saturated-rate figures above. A service consistently using 1 vCPU and 2 GB of memory around the clock reaches roughly $20 of CPU and $20 of memory per month at Railway's published rates, before egress. A Standard pack with the same 1 vCPU and 2 GB is $7/mo, plus $3/mo for the database. That gap is the entire argument, and it only exists for services that are genuinely running.

Invert it and the answer inverts too. Four services that are idle 95% of the time will not come close to consuming their $5 Hobby credit, and moving them to four fixed packs makes your bill go up. Run your own numbers in the Railway cost calculator before committing a day to this, and if the calculator says stay, stay.

Related

Ready to deploy?

Start free on Hostwares - your app live in 60 seconds.

Get Started Free