Railway Preview Deployments: Isolated Staging for Every Pull Request
Railway Preview Deployments create isolated, production-parity environments for every PR. Learn how environment references, private networking, and release commands work, plus the limits to plan for.
16 Sept 2025, 17:58 UTC

The shared staging trap
Most teams hit the same wall: CI passes on a PR, but the bug only appears when the code runs against a real database and a real service graph. Shared staging environments make this worse. One developer's migration breaks another's test run. Environment variables drift. The staging database fills with junk data from three different branches. You end up testing less because the environment is unreliable.
Railway Preview Deployments remove that bottleneck by creating a complete, isolated copy of your service graph for every pull request. The useful takeaway: if your project already builds on Railway, you can get production-parity testing per PR without maintaining a separate staging server or writing a complex CI matrix.
How preview environments stay isolated
When you push a commit to a branch with an open PR against a Railway-connected repository, Railway provisions a preview environment. It builds the same Nixpacks or Dockerfile definition as your production service. The preview receives a unique HTTPS URL, typically following a pattern like pr-42-myapp.up.railway.app, and updates on every force-push to that branch.
Configuration isolation comes from Railway's environment variable references. When you write ${{VAR}} in railway.toml or the dashboard, Railway resolves the reference against the environment currently deploying. That means a preview environment can point to a preview-scoped PostgreSQL instance while production points to its own database—no manual find-and-replace or config drift.
Networking mirrors production topology. Services inside a preview communicate over Railway's private internal mesh using hostnames like postgres.railway.internal or redis.railway.internal, not the public internet. This keeps the same service discovery behavior you rely on in production and avoids CORS or SSL surprises that appear when staging services talk over public URLs.
Worked example: running migrations before the preview goes live
A common failure mode is deploying code that expects a schema change while the preview database still has the old schema. You can gate the preview on a successful migration by defining a release command in railway.toml.
Create or edit railway.toml at the repository root:
# railway.toml [build] builder = "nixpacks" [deploy] releaseCommand = "npm run migrate" restartPolicyType = "ON_FAILURE" restartPolicyMaxRetries = 10
Commit this file to your repository. Railway reads it during the preview build. The releaseCommand executes after the build completes but before the service starts accepting traffic. If the migration fails, the preview deployment fails and the PR check remains red.
Your package.json needs a matching script:
"scripts": {
"migrate": "node ./dist/migrate.js",
"start": "node ./index.js"
}
When you push the PR, Railway should post the preview URL to the PR checks within a few minutes. You can then run integration tests or manual QA against that URL. The Railway CLI command railway preview can also trigger or inspect previews from your terminal, though you need to be authenticated with the Railway CLI and have access to the project.
Trade-offs to plan for
Preview environments are not free, and they carry operational limits you should verify against your current plan.
- Concurrent preview caps. Railway limits how many preview environments can run simultaneously per project. The research indicates defaults around 5 on Hobby and 20 on Pro, but these numbers are plan-dependent and change over time. Check Railway's current pricing documentation before relying on a specific number. Stale open PRs consume slots, so close or merge branches you no longer need.
- Empty databases by default. Preview databases start empty or from a snapshot; they do not clone production data. If your tests need realistic data volume, script anonymized seed data or import a dump using
railway connect. Be careful not to leak production secrets into preview logs during this process. - Environment variable leakage. Variables set at the project level in the Railway dashboard apply to every environment unless overridden. If you add a production API key at the project level, every preview environment inherits it. Audit variables per environment before merging changes that touch secrets.
- Build caching differences. Nixpacks builds can cache layers across previews, but Dockerfile builds may rebuild from scratch on each push unless you configure a remote cache. Large images will make preview spin-up slower.
Verify it works
Do not assume the preview pipeline is correct until you have seen it fail and recover.
- Create a test repository with a web service and a PostgreSQL database. Open a PR and confirm a preview URL appears in the PR checks.
- Add a deliberately failing migration to
releaseCommandand push. Confirm the preview deployment fails and the check stays red. - Add a second service such as Redis and confirm the preview app connects via
redis.railway.internalwithout exposing Redis publicly. - Run
railway preview listfrom your terminal to see active previews and cross-reference resource usage with the Railway dashboard.
If a preview URL is public and you are testing internal-only features, put authentication in front of the app rather than relying on the URL being unguessable. Railway's docs and pricing page are the authoritative source for current preview-hour allowances and concurrent limits.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.