Railway's documentation for isolating staging from production contains one sentence that most people building on Railway have not read: "every service with a public domain is reachable from the internet regardless of environment."
Your staging environment is not behind a wall. It is a second production, with a different URL and usually worse data hygiene.
TL;DR
A Railway environment isolates services, variables, volumes and the private network. It does not isolate who can see the project or who can reach the app. Duplicating production to make staging gives you a structural copy with a real public domain, an empty database, and (correctly) no sealed variables. The gap between "isolated" and "private" is where the exposure lives.
Railway environment: per Railway's docs, "isolated instances of all services in a project." Every project starts with one named production. "All changes made to a service are scoped to a single environment," so editing a variable in staging does not touch production.
What an environment actually contains
When you create a second environment, Railway gives you two options in the + New Environment dropdown or under Settings, Environments:
Duplicate Environment. Copies the selected environment "including services, variables, and configuration." Every service is staged for deployment and waits for your approval before it goes live.
Empty Environment. No services. You add what you want.
Duplicate is what nearly everyone picks, because a staging environment that structurally matches production is the entire point.
Worth knowing if you inherited an older project: Railway deprecated forked environments in January 2024 in favour of isolated environments with Sync. Environments forked before that change still exist and still behave the old way, so a project older than that may not match the current docs.
The four things that are isolated
| Isolated per environment | What that means in practice |
|---|---|
| Services | Each environment runs its own instances. A crash in staging does not touch production. |
| Variables | Same variable name, different value per environment. DATABASE_URL resolves to that environment's database. |
| Volumes and data | Sync copies configuration, not data. Databases, volumes, and storage bucket contents stay separate. A duplicated Postgres comes up empty. |
| Private network | Each environment gets its own encrypted Wireguard mesh. Services talk over private DNS without exposing ports. |
The private network detail is the one that trips people up in a good way. The internal DNS name is identical in both environments, so postgres.railway.internal works unchanged in staging and production and resolves to that environment's instance. Your code and your reference variables stay the same; only the environment they run in changes what they point at.
This is why "it works in staging" is a meaningful signal on Railway in a way it isn't everywhere. The connection string shape, the private DNS, and the variable names are genuinely the same. What differs is the data behind them.
The two things that are not
Railway's isolation guide is unusually direct about this. "Two things are shared across environments and need their own handling: project membership (addressed with Environment RBAC) and public networking (every service with a public domain is reachable from the internet regardless of environment)."
Public networking
Duplicate production and the copy inherits its service configuration, which includes public domains. Railway generates a new *.up.railway.app domain for the staging copy. That domain is live, indexable, and reachable by anyone who finds it.
Nothing about it says "staging" to a scanner. It runs the same code, exposes the same endpoints, and answers the same requests. If your production app has an admin route protected by an environment variable check, and staging's copy of that variable is a placeholder someone set to true while debugging, the admin route is open on a public URL.
Staging environments are where security controls get temporarily disabled and never re-enabled. Auth bypasses for testing, DEBUG=true, relaxed CORS, a seeded admin account with a password someone will recognise. All of it on a public domain that inherits nothing protective from being called "staging".
We see this constantly in scans: the production domain is clean and a forgotten preview or staging domain on the same platform answers with a stack trace. The environment boundary protected the data. It did nothing about the front door.
Project membership
On most plans, adding someone to a Railway project adds them to all of its environments. They can read production's variables because they can read the project.
Environment RBAC changes that, and it is an Enterprise feature. Marking production restricted means "non-admin members and deployers can see the environment exists but cannot view its variables, logs, metrics, services, or configuration." Useful, but only if you're on a plan that has it. Below that, treat every project member as a production secret holder, because they are.
Sealed variables and the sync boundary
Sealed variables are the one place Railway deliberately breaks the copy. Per the isolation guide, "sealed values are excluded from environment duplication, PR environments, environment sync diffs, and external integrations," so "production secrets never leak into staging through a sync."
That is the right behaviour, and it produces a confusing symptom. Duplicate production to create staging, deploy, and the service crashes at boot because STRIPE_SECRET_KEY is not merely wrong, it is absent. The variable name does not exist, so there is no fallback and no obvious clue.
Read the crash as the feature working. The fix is to set a staging value for that variable in the staging environment: a Stripe test key, a sandbox credential, a throwaway token. If you find yourself copying the production value across to make the error go away, you have manually recreated the leak Railway just prevented.
The general rule Railway's guide states is worth adopting even outside Railway: keep credentials environment-scoped. Same variable name, different value per environment, production keys only ever in production.
Setting up staging without copying production's problems
Duplicate production. Do not approve the staged deployments yet.
Go through the variable list before the first deploy. Every sealed variable is missing and needs a non-production value. Every unsealed secret was copied verbatim and needs replacing with a test credential. That second category is the dangerous one, because nothing breaks to remind you.
Decide whether staging needs a public domain at all. If it is only for you and CI, remove the generated domain and reach it over the private network or the Railway CLI. Removing the domain is the single highest-value step here, and it is one click.
If it does need to be public, put something in front of it. Basic auth middleware, an allowlist, or a shared token header. Any of them beats an unauthenticated copy of your app on a guessable subdomain.
Seed staging with generated data. Restoring a production dump into staging moves every real customer record onto that public domain and undoes the one isolation guarantee Railway gave you for free.
railway environment # interactive picker
railway environment staging # switch directly
railway variables # list what this environment resolves to
railway run npm start # run locally against this environment's variables
railway run is the fastest way to catch a variable that only exists in production. Run it against staging and watch what fails to start.
A ten-minute audit
For the mechanics of adding and scoping variables in the first place, the Railway environment variables guide covers the dashboard and CLI paths. If you have PR environments turned on, what they copy from production covers the ephemeral case, which has a different default base and a different failure mode.
What is a Railway environment?
Railway's docs define one as an isolated instance of all services in a project. Every project starts with one called production, and all changes made to a service are scoped to a single environment, so editing staging never touches production.
Does duplicating a Railway environment copy my database contents?
No. It copies the database service, not the rows in it. Railway's sync copies configuration rather than data, so volumes, database contents, and storage bucket contents stay separate per environment. A duplicated Postgres comes up empty and your migrations have to run.
Why is my variable missing after I duplicated an environment?
It was sealed. Railway excludes sealed values from environment duplication, PR environments, sync diffs, and external integrations, so the name is absent entirely rather than set to a wrong value. Set a non-production value for it in the new environment.
Is my Railway staging environment public?
If any service in it has a public domain, yes. Railway's isolation guide lists public networking as one of two things shared across environments: every service with a public domain is reachable from the internet regardless of which environment it lives in. Remove the domain if staging does not need one.
Can I stop teammates from seeing production variables?
Only with Environment RBAC, an Enterprise feature that lets you mark production restricted so non-admin members and deployers can see the environment exists but cannot view its variables, logs, metrics, services, or configuration. On other plans, project membership covers every environment in the project.
Found a staging URL you forgot about?
Point CheckYourVibe at it. The scanner reads it the way a stranger would, headers and all.