MySQL to Railway Postgres
This is an operator’s playbook for a MySQL to Railway Postgres migration. It covers the Railway-specific connection details — the public TCP proxy, self-signed TLS, and private networking — that you need to move MySQL into a Railway PostgreSQL service.
If you searched for how to migrate MySQL to Railway Postgres or move a MySQL database to Railway, the short version is: use Railway’s public proxy URL (DATABASE_PUBLIC_URL) with sslmode=require from an external host, run the migration inside Railway to avoid egress charges when you can, and let pgferry’s MySQL type mapping handle enums, sets, and unsigned integers.
What this guide is for
Section titled “What this guide is for”Use this guide when you have a live MySQL database and want it on a Railway-hosted PostgreSQL service. It assumes you have added a Postgres database to a Railway project. For source-side behavior that is not Railway-specific, read the generic MySQL to PostgreSQL guide alongside this page.
Why use pgferry instead of generic pgloader advice
Section titled “Why use pgferry instead of generic pgloader advice”pgloader is the usual “mysql to railway postgres” suggestion, and it falls down on real schemas:
- No resume — a drop over Railway’s TCP proxy means restarting the whole load.
pgferrycheckpoints and resumes. - MySQL enums, sets, unsigned integers,
tinyint(1), and zero dates are explicit, documented knobs inpgferry. pgferrystreams with chunked, parallelCOPYand runs aplanpreflight that surfaces skipped indexes, generated columns, and required extensions first.
Destination prerequisites
Section titled “Destination prerequisites”- A Railway project with a Postgres service. Default database is
railway, default user ispostgres. - The connection variables from the service’s Variables tab. Railway’s Postgres image runs as a single-tenant container, so the
postgresrole has broad privileges —CREATE SCHEMA,CREATE EXTENSION(for extensions whose binaries ship in the image), etc. - For pgvector or other non-default extensions, deploy the matching Railway template/image variant — the base image only carries the standard contrib set.
Recommended pgferry config
Section titled “Recommended pgferry config”schema = "app"on_schema_exists = "error"unlogged_tables = falseresume = truevalidation = "row_count"chunk_size = 100000source_snapshot_mode = "single_tx"
[source]type = "mysql"# dsn supplied via PGFERRY_SOURCE_DSN
[target]# dsn supplied via PGFERRY_TARGET_DSN
[type_mapping]tinyint1_as_boolean = falsejson_as_jsonb = trueenum_mode = "check"set_mode = "text"sanitize_json_null_bytes = trueresume = true requires unlogged_tables = false (see the configuration reference).
Railway DSN, TLS, proxy, and networking notes
Section titled “Railway DSN, TLS, proxy, and networking notes”Railway exposes two connection URLs on the Postgres service:
| Variable | Host shape | Use from |
|---|---|---|
DATABASE_URL (private) | postgres.railway.internal:5432 | Another service inside the same Railway project |
DATABASE_PUBLIC_URL (public) | <name>.proxy.rlwy.net:<random-port> | An external migration host |
- From your laptop or any external host, use
DATABASE_PUBLIC_URL. The.railway.internalhost is not routable outside Railway. The public proxy port is randomly assigned — never hardcode5432for the external endpoint; read it from the variable. - TLS: Railway’s Postgres image uses a self-signed certificate. Use
?sslmode=require(encrypts the connection without certificate-chain verification).sslmode=verify-fullwill fail because Railway does not publish a CA for the auto-generated cert. - Egress: traffic through the public proxy leaves Railway’s network and counts toward egress/network billing. A multi-GB migration over the proxy can add up.
- Private networking: if you run pgferry as a service inside the same Railway project, use
DATABASE_URL(the internal host) — no egress charges, lower latency. Note the private network is IPv6-based; the Go/pgxdriver pgferry uses handles this as long as the host resolves.
Example external (public proxy) target DSN:
export PGFERRY_TARGET_DSN='postgres://postgres:<password>@<name>.proxy.rlwy.net:<proxy-port>/railway?sslmode=require'export PGFERRY_SOURCE_DSN='user:pass@tcp(mysql-host:3306)/source_db'Inside Railway (same project, no egress):
export PGFERRY_TARGET_DSN='postgres://postgres:<password>@postgres.railway.internal:5432/railway?sslmode=require'The private network is isolated to your Railway project, so sslmode=disable also connects internally — but Railway’s image serves SSL on the same port, so keep sslmode=require to encrypt the wire with no practical downside.
Capacity gotcha
Section titled “Capacity gotcha”The Railway Postgres service is backed by a volume with a plan-dependent size cap, and WAL plus indexes built during the load consume extra space transiently. Pre-size or upgrade the volume before a large import so you do not run out mid-load.
Source-specific caveats (MySQL)
Section titled “Source-specific caveats (MySQL)”Decide these on the MySQL side (full detail in the MySQL guide):
enum_mode/set_mode— howENUM/SETland in PostgreSQL.tinyint1_as_boolean— only iftinyint(1)means boolean.widen_unsigned_integers/add_unsigned_checks— preserve unsigned ranges.zero_date_mode—0000-00-00→NULLor error.- Generated columns copy as values;
FULLTEXT, prefix, and expression indexes are reported and skipped. ci_as_citext = trueneeds thecitextextension (present in the base image’s contrib set).
Step-by-step MySQL to Railway Postgres migration flow
Section titled “Step-by-step MySQL to Railway Postgres migration flow”- Add Postgres to your Railway project and copy
DATABASE_PUBLIC_URL(or use the internal URL if running inside Railway). - Confirm the volume is large enough for the dataset plus index/WAL overhead.
- Generate a config with
pgferry wizardor start from the snippet above. - Export
PGFERRY_SOURCE_DSNandPGFERRY_TARGET_DSN. - Run
pgferry plan migration.tomland resolve every warning. - Run
pgferry migrate migration.toml; rerun on interruption (resume = true). - Recreate views, routines, and triggers via hooks.
Validation and cutover checklist
Section titled “Validation and cutover checklist”pgferry validate migration.tomlre-runs validation without redoing DDL orCOPY.- Confirm required extensions exist (and that you used the right image variant for pgvector etc.).
- Verify the volume has headroom after the load.
- Spot-check enum/set and
tinyint(1)columns. - Walk the cutover checklist and first production migration checklist.
Common failures for this provider pair
Section titled “Common failures for this provider pair”| Symptom | Cause | Fix |
|---|---|---|
could not translate host name "postgres.railway.internal" | Used the private host from outside Railway | Use DATABASE_PUBLIC_URL |
| TLS / certificate verification error | verify-full against a self-signed cert | Use sslmode=require |
Connection refused on :5432 externally | Hardcoded port instead of proxy port | Use the random proxy port from DATABASE_PUBLIC_URL |
No space left on device mid-load | Volume too small for data + indexes | Pre-size/upgrade the volume |
| Unexpected network bill | Bulk load over the public proxy | Run the migration inside Railway (private network) |
See common failures and recovery.
Related
Section titled “Related”- MySQL to PostgreSQL — generic source guide
- Configuration reference
- Type mapping
- MySQL minimal-safe example
- Cutover checklist · First production migration checklist
- Other destinations: MySQL to Supabase · MySQL to Neon · MySQL to Render Postgres · MySQL to PlanetScale Postgres