Stop Exposing Your Database: Railway Private Networking for App + Postgres
Railway's private networking lets services in the same project talk over an internal network. Here's how to wire an app to Postgres without ever exposing the database publicly.
24 Dec 2025, 21:20 UTC

If you deploy a web app and a Postgres database on Railway and connect them through the database's public proxy domain, your database is reachable from the entire internet. Authentication still protects it, but you've added attack surface, extra latency, and a public hostname you never needed. The fix is Railway's private networking: services in the same project can talk to each other over an internal network, so the database never needs a public endpoint at all.
This post walks through when private networking matters, how to wire an app to a database with it, and the trade-offs you should know before relying on it. One caveat up front: Railway's exact variable names and UI labels have changed over time, so treat the names below as indicative and confirm them against the current docs for your project.
What private networking actually gives you
Railway organizes deployments into projects and environments. Every service you deploy can optionally get a public domain, but services in the same project also get internal DNS names that only resolve inside Railway's network. When your API connects to Postgres using that internal hostname instead of the public proxy host, three things improve:
- Smaller attack surface. If the database has no public domain, there is nothing for a random scanner to find. Only sibling services in the same project can reach it.
- Lower latency. Traffic stays on the internal network instead of hairpinning out through a public proxy and back in.
- Fewer surprises with egress. Routing through public endpoints can interact with egress limits or costs depending on your plan. Internal traffic sidesteps that question, though exact billing behavior is plan-sensitive, so verify for your tier.
One thing private networking does not do: replace authentication. Internal-only services should still use strong credentials and least-privilege database users. "Not publicly routable" is not a security model by itself.
Worked example: Express API plus Postgres
The setup: one Railway project containing two services — a Node/Express API and a Postgres database. The goal is for the API to reach Postgres internally, with no public domain on the database.
- Deploy Postgres first. Add a database service to the project. By default Railway may provision connection variables for both public and private access.
- Remove the public domain from the database. In the service's networking settings, delete or never generate a public domain. After this, the database is only reachable via its internal hostname from sibling services.
- Reference the database variables in the app service. Railway supports reference variables, so one service can consume another's variables without you pasting credentials anywhere. In the app service, set
DATABASE_URLto reference the database's connection string, making sure it uses the private host and port rather than the public proxy host and port. The internal host is typically exposed as a private-domain-style variable on the database service. - Deploy the app. It should boot and connect without any code changes — the only difference is which hostname
DATABASE_URLpoints at.
The most common failure mode here is mixing up hostnames: Railway shows both a public proxy host/port and an internal host/port for the same database, and they are not interchangeable. If the app hangs or refuses connections after you remove the public domain, check which one your connection string is using.
How to verify it's actually private
Don't just assume the configuration worked — check it:
- From the app service, open a shell and resolve the database's internal hostname (for example with
nslookupor a quick connection test). It should resolve to an internal address and accept a connection on the database port. - From your local machine, try to connect to that same internal hostname. It should fail to resolve or be unreachable. If you can reach the database from your laptop, something is still publicly exposed.
- Confirm the app still works end to end — hit an endpoint that queries the database and confirm data flows.
Be aware that internal addressing and region placement can affect connectivity assumptions, and details may vary by plan. If something doesn't behave as described here, check Railway's current docs and changelog before debugging your own code.
The trade-off: same project only
The main limitation is scope. Private networking only works between services in the same project and environment. If you later split services across projects — say, a shared database used by two separate application projects — those services can't use the internal network to reach each other. You'd be back to public endpoints (with TLS and strong auth) or another networking approach.
It also means your local development workflow doesn't change: you still can't reach the internal database from your laptop, so for local debugging you'll either use a tunnel, a temporary public domain, or a local database instance.
Actionable takeaway
If your Railway project has a database with a public domain that only your app uses, remove it. Switch the app's connection string to the internal hostname via reference variables, verify from a service shell that the internal host resolves, and verify from your own machine that it doesn't. That's roughly fifteen minutes of work for a permanently smaller attack surface and lower-latency connections. Just confirm the current variable names in Railway's docs first — the platform moves fast, and the details matter more than the concept.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.