Skip to main content

Railway's Databases Went Private by Default: Migrating Every Connection String That Assumed a Public Proxy

11 min readDora NodaDora Noda
Share
On this page

Railway stopped handing every new database a public address. On July 31, 2026, the four core database templates — Postgres, MySQL, MongoDB, and Redis — started deploying with no TCP proxy by default, which means no public connection string unless you explicitly opt in. If your local psql, your BI export, or your 2 AM incident runbook connected over a *.proxy.rlwy.net URL, here is the one-line summary: your existing services are untouched, but every new database deploy is private-only until you flip Settings → Networking → Public Access, and the fix per access pattern is a five-minute change once you know which variable you are supposed to be reading.

That is the whole migration up front, in table form. The rest of this post walks each row, explains why Railway made the trade, and prices the same Postgres on hardware you own — where nobody can deprecate your connection string.

Access patternSymptom after the changeFix (minutes)Variable to use
App + DB both on RailwayWorked before, still works — but only on the private URLPoint the app at the private URL if it is not alreadyDATABASE_URL (*.railway.internal)
Local psql / GUI (TablePlus, DataGrip, Compass)No public host:port to paste into the clientEnable TCP proxy on the DB service, connect with SSL requiredDATABASE_PUBLIC_URL
Migrations / seeds run from a laptopmigrate deploy times out reaching the DBOpt-in proxy for the migration run, or run migrations via railway run / railway ssh on the private networkDATABASE_PUBLIC_URL for the laptop; DATABASE_URL inside Railway
BI tool / nightly export jobExternal extractor can no longer reach the databaseRe-enable the proxy for that service, or move the extractor onto Railway so it uses the private networkDATABASE_PUBLIC_URL (proxy) or private URL from a Railway service
Incident runbook psqlThe pasted connection string in the runbook is gone on new deploysUpdate the runbook: proxy-enable step plus the public URL, or shell into Railway firstEither path — decide once, write it down
External (non-Railway) appNo route in at allExplicit proxy re-enable; treat the public endpoint as provisioned surface, not default*_PUBLIC_URL

What actually changed

The evidence is in Railway's own docs diff: PR railwayapp/docs#1280, titled "databases deploy private by default," rewrote the Connecting externally sections of all four database guides (postgresql.md, redis.md, mysql.md, mongodb.md). The old sentence — the TCP proxy "is enabled by default" — was replaced with the new flow: databases deploy private, and public access is opt-in from the service's Settings → Networking (Public Access), which creates the TCP proxy and populates the *_PUBLIC_URL variable.

Concretely, a fresh Postgres deploy now gives you two different URLs with two different reachabilities:

  • PrivateDATABASE_URL, resolving to postgres.railway.internal:5432. Reachable only from services inside the same Railway project and environment. No TLS hop over the public internet, no public port.
  • PublicDATABASE_PUBLIC_URL, a *.proxy.rlwy.net:<port> (or *.railway.app:<port>) endpoint that forwards raw TCP to the database's internal port. Only exists after you enable it. Connections from outside Railway must use SSL (sslmode=require on Postgres).

Three scope notes that keep this from becoming a scare post. First, existing services are untouched — the change applies to new template deploys, not a retroactive removal from databases you already run. Second, the in-project data path never needed the proxy at all: app-to-database traffic over *.railway.internal skips the TLS-plus-public-internet hops the proxy adds to every query. Third, the community already treats proxy-off as the steady state — one verified deployment log notes its Postgres and Redis public proxies were removed on August 1, 2026 with health checks green throughout, and community deploy guides now end with "once you are on the private network, disable the public TCP proxy and delete the leftover DATABASE_PUBLIC_URL."

So the blast radius is exactly one population: anyone whose workflow reached the database from outside Railway using a connection string that used to exist by default.

Migrating each broken pattern

1. App-to-database inside Railway: do nothing, then verify

If your app service and your database live in the same Railway project, the private reference variable (e.g. ${{Postgres.DATABASE_URL}}) already resolves over the private network. This pattern never traversed the proxy. Post-change audit: open the app service's variables and confirm nothing reads DATABASE_PUBLIC_URL. If something does, switch it to the private URL and delete the proxy — one fewer public port, slightly lower per-query latency.

2. Local psql and GUI clients: re-enable the proxy, require SSL

This is the pattern the changelog breaks most visibly — the developer who debugged production from a laptop with a pasted connection string.

  1. Open the database service → SettingsNetworking → enable the TCP Proxy (labeled Public Access / Add Public Access in current docs).
  2. Railway generates a public host:port pair and sets DATABASE_PUBLIC_URL on the service.
  3. Connect with SSL required. For Postgres:
text
postgresql://postgres:<password>@<name>.proxy.rlwy.net:<proxy-port>/railway?sslmode=require

The same shape holds per engine: MySQL clients build the URL from the public host/port/user/password (external hosts need the public URL; the MYSQLHOST-style split variables describe the private side), MongoDB Compass takes the public host:port, and redis-cli -u takes the REDIS_PUBLIC_URL once the proxy targets the Redis protocol port.

Cost of the fix: one click plus remembering that the public endpoint is now a resource you provision deliberately. Which is the point — see the next section.

3. Migrations and seeds from a laptop: pick a lane

prisma migrate deploy, Drizzle migrations, or a seed script run from your shell needs a route to the database, and there are now exactly two sanctioned ones:

  • Public lane: enable the proxy and export DATABASE_PUBLIC_URL as DATABASE_URL for the migration command. Fine for one-off runs; you are paying public-internet latency for a handful of DDL statements.
  • Private lane: run the migration from inside Railway — railway run (which injects the private environment) or railway ssh -s <service> followed by the migrate command. No proxy needed, no public surface at all.

Teams that migrate often should standardize on the private lane and stop maintaining a proxy just for deploys. Teams that migrate rarely should keep the proxy off and enable it only for the migration window. Either way, "migrations need the public URL permanently on" is no longer the default answer.

4. BI tools and export jobs: proxy or relocate

A Metabase instance, a nightly pg_dump cron, or a sync script on someone's laptop are all external clients, so they need the proxy — unless you move the extractor onto Railway, where the private URL works and nothing crosses the public internet. The decision rule is simple: if the extractor already lives outside Railway and cannot move, re-enable the proxy and point it at *_PUBLIC_URL. If it is a script you own, deploying it as a Railway service (even a cron-triggered one) removes a public database port from your architecture permanently.

5. Incident runbooks: rewrite the psql line before the incident

The most dangerous breakage is not a developer noticing on a Tuesday — it is an on-call engineer opening the runbook at 2 AM and finding a connection step that assumes a public endpoint exists on a database that was provisioned after the default flipped. Audit every runbook that contains proxy.rlwy.net, a DATABASE_PUBLIC_URL paste, or "connect with your local client," and replace it with one of the two lanes above. The railway ssh path deserves emphasis here: shelling into Railway first and using the private URL works even when every proxy is off, which is exactly the posture you want during a security incident.

Why Railway did it: every default proxy is attack surface

Strip away the changelog politeness and the trade is legible: a default-on TCP proxy means every database ever deployed from a template — including the abandoned prototypes, the staging copies, the "I will delete this tomorrow" experiments — listens on a public host:port guarded only by a password in a connection string. That string gets pasted into laptops, CI logs, BI configs, and chat threads. Each copy is a credential with a network route attached.

Private-by-default inverts the burden of proof. The database starts unreachable from the internet; every public endpoint that exists is one a human provisioned, named, and can enumerate in the dashboard. The vendor shrinks its fleet-wide exposed-port count by orders of magnitude at the cost of one extra click per team that genuinely needs external access. From Railway's side of the ledger — abuse handling, credential-stuffing noise, incident response against public database ports — that is an overwhelmingly good trade.

From your side, the cost is real but bounded: the five migrations above, each a one-time change, plus a new habit of treating *_PUBLIC_URL as provisioned surface rather than ambient furniture. The teams who feel it most are the ones with the most external access patterns — which is another way of saying the teams with the most exposure the new default just removed.

What the same Postgres costs where nobody can deprecate your connection string

There is a second cost worth pricing, because it compounds the first: every access pattern above exists at Railway's discretion. The proxy, the private DNS, the variable names, the Settings tab they live under — all of it can change again with another changelog entry. On hardware you own, the connection string changes when you change it.

What you are comparingRailway (metered)Owned Hetzner-class box (flat)
1 vCPU / 1 GB database-ish slice, 24/7≈ $30/month ($20/vCPU + $10/GB RAM, billed per second)A whole 2 vCPU / 4 GB box ≈ €5–6/month
Plan fee + 100 GB volume+$5/month plan, +$15/month volume ($0.15/GB)Included SSD + 20 TB included bandwidth
Quiet Postgres + app + Redis≈ $12–25/month across metered services≈ $6/month on one box, all three services
Typical load with a worker≈ $25–45/monthSame box, more utilized — still ≈ $6–12/month

The math is not subtle: a single small database slice on Railway's meter costs roughly what an entire 4 GB box costs on Hetzner, and the box also holds your app, your Redis, and your worker with 20 TB of included egress. What the meter buys — and this post's companion piece on Railway's managed-data bundle prices it line by line — is freedom from patch windows, failover wiring, backup verification, and connection pooling. That is a real service. It is just not the same service as "a Postgres whose network topology nobody else can redesign."

The honest framing for a team doing migration math: Railway's meter charges for operations you do not do; the owned box charges a flat fee for control nobody can take away. Private-by-default is a good security decision that slightly raises the salience of the second bill — every proxy re-enable is a small reminder that the network your database lives on belongs to someone else's roadmap.

The audit checklist

Before your next database deploy, work through this once:

  1. List every *_PUBLIC_URL reader: local clients, GUIs, migration commands, BI tools, export crons, runbook steps, external apps.
  2. For each reader, decide: private-network (move it onto Railway) or explicit proxy (enable Public Access and document why).
  3. Delete every proxy with no documented reader. The dashboard should show zero public database endpoints you cannot explain.
  4. Rewrite the incident runbook's database-access section for the world where new databases have no public URL.
  5. If the count of "must stay public" endpoints keeps growing — BI fleet, external services, compliance exports — price the owned-box column above. There is a threshold where a flat machine with a firewall you control stops being a hobbyist move and starts being the boring, correct answer.

Railway's private-by-default change is the right default — and a reminder that every default you inherit can flip. Bex.co is the open-source, AI-native Render alternative: push a git repo, get a running HTTPS service on machines you own, where the database topology changes when you change it. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex