如何逐步把應用程式從 Railway 遷移至 Hubfly space
How to move your app from Railway to Hubfly space, step by step
文章逐步説明如何把 Railway 的網頁服務和 Postgres 資料庫遷移至 Hubfly space,透過 Compose 檔案部署、一次性工作複製資料,並在短暫唯讀時段切換網域。
Originally published on the Hubfly space blog.
TL;DR
- What you get: your Railway web service and Postgres database running on Hubfly space, your data copied with one throwaway job, and your domain switched over with minutes of read-only time, not hours of downtime.
- Who it's for: anyone with a Railway project made of a web service, a Postgres database and maybe a volume or a cron job.
- Time needed: about 45 minutes for a small app, most of it waiting for DNS.
- Cost on Hubfly space: about $7.31 a month for a web service and Postgres, each on 1 vCPU and 512 MB, running 24/7, plus 1 GB of disk. The math is below.
Your app runs on Railway and it works. But the bill moves with your traffic, and Railway's railway.json and railway.toml files stop working on 1 December 2026 (Railway docs). If you use config-as-code, you'll have to rewrite it either way. That makes now a good moment to decide where the app should live.
This guide moves a typical Railway project (a web service plus Postgres) to Hubfly space. You'll export your variables, describe the app in one Compose file, copy the database with a one-shot job that runs inside your new project, and switch your domain with a short write freeze. Your users see a few minutes of read-only mode at most, and you can roll back by pointing DNS back at Railway.
Try it on Hubfly space: the free tier includes 360 compute hours a month and needs no card, so you can rehearse the whole move before you pay anything.
Before you start: what moves, and where it goes
Write down every piece of your Railway project first. Anything you forget here becomes an outage later.
| On Railway | On Hubfly space | How it moves |
|---|---|---|
| Service built from GitHub | Container built from the same repo, or a Compose service | Connect the repo or point Compose at an image |
| Postgres database service |
postgres:16-alpine container with a volume |
pg_dump piped into pg_restore (step 4) |
Variables, including ${{Postgres.DATABASE_URL}} references |
Compose environment: or project variables, with {{service:db.host}} references |
Export, then rewrite references (step 1) |
| Volume (one per service) | Volume mounted at the same path | Archive and upload (step 5) |
Private hostname web.railway.internal
|
{{service:web.host}} |
Replace in variables |
| Custom domain (CNAME + TXT) | Add the domain to the container | DNS switch (step 6) |
cronSchedule service |
A job container or an always-on worker | Re-create |
Step 1: Export your variables
Variables are the easiest thing to lose. Export them per service with the Railway CLI (CLI reference):
railway link # pick the project and environment
railway variables --service web --kv > web.env
railway variables --service Postgres --kv > postgres.env
Open web.env and check three things:
- Sealed variables are missing. Railway never shows sealed values in the UI, the API or the CLI (Railway docs). Find them in your password manager or your provider's console (Stripe, Resend, OAuth apps) and add them by hand.
-
Railway-only variables can go. Drop
RAILWAY_*entries. KeepPORTonly if your app reads it. -
Database URLs point at Railway. The exported
DATABASE_URLholds Railway's private hostname, which won't resolve anywhere else. You'll replace it in step 3.
Why this matters: a missing secret doesn't fail the deploy. It fails the first checkout or login after you switch DNS, in front of real users.
Step 2: Create the project
- In the Hubfly space dashboard, click Create Project.
- Name it after your app (
orders-api), choose Cell, and pick the region closest to your users (eu-central, eu-north or usa-central). - Open Advanced Options and set a Monthly Budget Limit. $10 is plenty for the rehearsal. It keeps a mistake from turning into a surprise bill.
- Optional: add shared values (an API key every service needs) under Project Environment Variables. They're encrypted with AES-256-GCM and are shared across the project.
Step 3: Describe the app in one Compose file
Railway builds each service separately. On Hubfly space you describe the whole app in one Compose file, and the Stack Composer deploys it in order.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: change-me-long-random
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres -d app"]
interval: 5s
timeout: 3s
retries: 20
migrate:
image: postgres:16-alpine
restart: "no"
environment:
SOURCE_URL: <your Railway DATABASE_PUBLIC_URL>
TARGET_URL: postgresql://postgres:change-me-long-random@{{service:db.host}}:5432/app
command: ["sh", "-c", "until pg_isready -d \"$SOURCE_URL\" && pg_isready -d \"$TARGET_URL\"; do echo waiting; sleep 2; done; pg_dump \"$SOURCE_URL\" --format=custom --no-owner --no-acl | pg_restore --no-owner --no-acl --exit-on-error -d \"$TARGET_URL\" && psql \"$TARGET_URL\" -t -c 'SELECT count(*) FROM orders'"]
depends_on:
db:
condition: service_healthy
volumes:
pgdata: {}
What each part does:
-
dbis your new Postgres. The named volume keeps its files across redeploys; without it, every update would wipe your data. -
The health check uses
-h 127.0.0.1. That forces a TCP check. Postgres briefly runs a temporary server on its Unix socket during first start, and a socket-only check can pass before the real server listens. -
migrateis a one-shot job (restart: "no"becomes a Job). It waits until both databases answer, streams a dump from Railway straight into the new database, and prints a row count you can compare. No laptop in the middle, no dump file to store. -
{{service:db.host}}is Hubfly space's version of Railway's${{Postgres.DATABASE_URL}}. It resolves to the database's private hostname, inside a longer URL too. - Replace
ordersin the last command with one of your own tables.
Add your web service under services: the same way, with depends_on: db: condition: service_healthy and DATABASE_URL: postgresql://postgres:change-me-long-random@{{service:db.host}}:5432/app. If you'd rather build from GitHub than from an image, create the web container from your repo after the stack is up; a root Dockerfile wins over auto-detection.
Don't commit the real password. After the first deploy, open each container's Configuration → Environment Variables → Edit and switch the database variables, including SOURCE_URL, to Secret. Variables are edited one at a time there, so paste longer lists into the Compose file before you import it.
Step 4: Copy the database
First, give the copy job a way in. In Railway, open your Postgres service, enable public access through the TCP Proxy, and copy the DATABASE_PUBLIC_URL it creates (Railway Postgres docs). Paste it as SOURCE_URL.
Then rehearse:
- In your project, click Create → Compose Stack, open the YAML tab, paste the file and click Import YAML.
- Click Deploy Stack. The activity log shows the volume, then
db, then the health gate, thenmigrate. A two-service stack like this one finishes in about 40 seconds. - Open the
migratecontainer → Logs. You should seeaccepting connectionsfor both databases, then your row count.
How long the real copy takes depends on bandwidth, not Postgres. Inside one region, a 24 MB database with 200,000 rows copies in about 2 seconds. Across the internet from Railway, the job's speed is set by the container's network link: a shared-tier container shows a 15 Mbps limit under its Network tab, roughly 2 MB per second. Custom-format dumps are compressed, so at 15 Mbps a 1 GB dump takes at most about 9 minutes. Run the rehearsal to get your real number. The job only runs for minutes, so the shared tier fits it. Railway bills proxy traffic as egress at $0.05 per GB (Railway pricing), so a 1 GB copy costs about 5 cents there.
Why rehearse: the dry run tells you exactly how long your write freeze will be, and it catches extensions or roles your new database is missing while nothing is at stake.
Step 5: Move volume files (skip if you only use Postgres)
Railway allows one volume per service. If yours holds uploads or a SQLite file, download the files with railway volume files or over scp through railway ssh (Railway volume docs). Then mount a volume at the same path on Hubfly space and copy the files into it, for example from a one-shot job that downloads the archive.
If those files are user uploads, consider moving them to Hubfly space Storage instead. It's S3-compatible, so your app's S3 client works with a new endpoint and keys, and it costs about $0.011 per GB-month.
Step 6: Switch the domain with a short write freeze
- A day before: lower your domain's DNS TTL to 60–300 seconds, so the switch spreads in minutes.
-
Add the domain on Hubfly space: add
app.your-domain.comto your web container, create the DNS records Hubfly space shows you, and wait for the certificate to be issued. Let's Encrypt issues and renews it for you. - Freeze writes on Railway: put the app in maintenance mode, or stop the web service. Anything written after the final dump would be lost.
- Run the final copy into a clean database: open Project Settings → Danger Zone → Delete All Resources (the Delete Everything button) to remove the rehearsal containers and volume, then import and deploy the same Compose file again. Compare the job's row count with Railway's, then turn off public access (the TCP Proxy) on Railway's Postgres.
- Switch DNS to the Hubfly space target and watch the web container's logs for real traffic.
- Clean up later: keep Railway running read-only for a few days as your rollback, then delete its CNAME and TXT records (both are required on Railway) and the Railway services.
Rollback is simple while Railway still exists: point DNS back. Plan to roll back only before new writes land on Hubfly space; after that, copying new data back is a second migration.
What it costs: Railway vs Hubfly space
Prices as of 20 October 2026. Railway bills measured usage per second (railway.com/pricing); Hubfly space bills the size you reserve per hour (hubfly.space/pricing).
| Setup (web + Postgres, 730 h, 1 GB volume) | Railway Hobby | Hubfly space |
|---|---|---|
| Light use: 0.15 vCPU and 0.45 GB RAM in use on average | $7.77 (the $5 plan fee is absorbed into usage) | $7.31 (two 1 vCPU / 512 MB containers, 24/7) |
| Busy: both services using 1 vCPU and 0.5 GB nonstop | $50.88 | $7.31 (same reserved size) |
| Staging copy that sleeps most of the day | Pays for active time; a slept service accrues no compute charges (docs) | Shared tier, free within 360 compute hours a month |
Railway math (CPU $0.00000772 per vCPU-second, RAM $0.00000386 per GB-second, volume $0.00000006 per GB-second; 730 h = 2,628,000 s):
- Light: 0.15 × 0.00000772 × 2,628,000 = $3.04; 0.45 × 0.00000386 × 2,628,000 = $4.56; 1 × 0.00000006 × 2,628,000 = $0.16. Total $7.77.
- Busy: 2 × 0.00000772 × 2,628,000 = $40.58; 1 × 0.00000386 × 2,628,000 = $10.14; volume $0.16. Total $50.88.
Hubfly space math (CPU $0.004 per vCPU-hour, RAM $0.002 per GB-hour, storage $0.00001536 per GB-hour):
- One container: 1 × 0.004 × 730 = $2.92; 0.5 × 0.002 × 730 = $0.73. That's $3.65.
- Two containers: $7.30. Volume: 1 × 0.00001536 × 730 = $0.011. Total $7.31. Faster volume modes (Balanced 1.5×, High Performance 2×) add up to about a cent per GB a month ($0.017 and $0.022 instead of $0.011).
The honest read: for a small, quiet app the two bills land within a few cents of each other. Hubfly space wins when your app does steady work, because the price doesn't climb with load. Railway wins when your services idle near zero most of the month.
Where Railway is still the better pick
Fair is fair. Stay on Railway, or move only part of your app, if you depend on:
- PR environments: Railway spins up an isolated copy of your environment for every pull request and deletes it when the PR is merged or closed (docs).
-
Built-in database backups and point-in-time restore, plus in-place Postgres major-version upgrades with automatic backups and one-click revert (docs). Hubfly space doesn't document database backups yet, so schedule your own
pg_dumpto Storage if you move. - Paying almost nothing for idle services: with per-second usage billing, a mostly idle app can cost less than a reserved container.
- More regions and multi-region replicas: Railway lists four regions across the US, Europe and Asia (regions) and can place replicas in several of them (scaling). Hubfly space offers three regions today.
What can go wrong
-
no responseorName does not resolveon the first connection. A fresh container can take a couple of seconds before private DNS answers. Theuntil pg_isready …; do sleep 2; doneloop in the job handles it. Without the loop, the job fails, the stack rolls back, and the job's logs go with it. -
The health check passes too early.
pg_isreadywithout-h 127.0.0.1can succeed against Postgres's temporary startup server. Keep the-h 127.0.0.1flag so dependents wait for the real one. -
${{...}}or$$copied as-is. Railway's${{Postgres.DATABASE_URL}}means nothing elsewhere; replace it with a full URL using{{service:db.host}}. And write$VARin Compose commands, not$$VAR: the Stack Composer keeps$$literally, and the shell turns it into a process ID.
Takeaway
A Railway move is mostly bookkeeping: export the variables, rewrite the references, copy the data with a job that runs next to the new database, and switch DNS during a short freeze. Rehearse once on the free tier and the real cutover is a few minutes of read-only mode.
Read next:
- Best Heroku alternatives in 2026: 7 options and what they cost
- How to deploy Odoo on Hubfly space with PostgreSQL and persistent storage
- Let your coding agent deploy to Hubfly space safely
Go further on Hubfly space
- Put uploads in Hubfly space Storage: S3-compatible buckets with image resizing built in (docs).
- Reach the database from your laptop without opening it to the internet, with a HubTunnel from the Hubfly space CLI (tunnels docs).
- Deploy automatically on every push with deploy webhooks and auto-rollback (deployment pipelines).
Ready to move? Create a free Hubfly space project, paste the Compose file above, and run your first rehearsal copy today. Moving a bigger Railway project? Talk to us about help with the cutover.
來源:DEV Community · dev.to