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. After Railway's October 21, 2026 security incident, the same per-environment scoping means a credential rotated in production keeps its old value in every other environment until you rotate it there too.
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.
Rotating after the October 2026 incident: one environment at a time
On October 21, 2026, Railway disclosed that an unauthorized third party may have accessed encrypted environment variables and decrypted some of them. Its preliminary report puts the scope at "at most 1.85% of all workspaces", about 100,000, and still recommends that every customer who signed up before October 21 rotate the keys they store on Railway. One of the two risk buckets it names is "Environments with a high amount of AI keys from vendors."
The unit there is the environment, and that matters because of the isolation table above. Railway's guide says it plainly: "Every variable value is scoped to one environment." So rotation is per environment too, and the copies you forgot about don't update themselves.
Here's what that means in a project where staging was duplicated from production:
- Unsealed secrets were copied verbatim. If staging got production's
OPENAI_API_KEYon the day you duplicated it, staging still holds that exact string. Revoke the old key at the provider (Railway's advice is to revoke, not just replace) and staging will start failing. Good. That failure is the tell that staging was running on a production key. Give it a test or low-limit key, not the new production one. - Staging's database has its own password. A duplicated environment gets its own Postgres service with its own credentials, so clicking Regenerate on production's database leaves staging's password exactly as it was. Railway's PostgreSQL rotation guide applies to each database service separately.
- Public database access is the multiplier. The same guide warns that with Public Access on, the credential "is usable from anywhere on the internet through the TCP proxy until you rotate", and the preliminary report's first recommendation is to remove public network access to databases. A staging database with a TCP proxy is on the internet like any other staging service with a public domain.
- Sealed doesn't mean skipped. The disclosure doesn't say sealed variables were treated differently, so roll them too. Sealing kept them out of the staging copy; it doesn't tell you whether production's sealed value was in scope.
The practical order: switch to each environment in turn (railway environment staging), run railway variables, and work down the list. The Railway environment variables guide covers editing and redeploying each value once you've issued a new one at the provider.
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.
Do I need to rotate staging variables after the Railway security incident?
Yes. Every Railway variable value is scoped to one environment, so rotating a key in production leaves staging's copy, and staging's own database password, untouched. Railway recommends that everyone who signed up before October 21, 2026 rotate the keys they store on Railway, and that covers every environment in the project, not just production.
Found a staging URL you forgot about?
Point CheckYourVibe at it. The scanner reads it the way a stranger would, headers and all.