Railway Environment Variables: Complete Setup Guide (2026)

How-To Guide

Manage secrets and configuration in Railway

Railway gives you four places to put a variable: per-service, project-wide Shared, ephemeral PR environments, and built-in RAILWAY_* values that Railway sets for you. Pick the wrong one and you'll either leak a key into a public PR preview or paste the same Stripe secret into six services and forget one when you rotate it.

This guide walks the dashboard UI step-by-step, then covers the four sources, the ${{...}} reference syntax, the CLI, and the gotchas that bite vibe-coded apps deploying their first paid service.

TL;DR

TL;DR: In Railway, click your service, open the Variables tab, and add key-value pairs. Railway auto-redeploys on save. Reference values across services with ${{SERVICE.VAR}}. Use Shared Variables for keys multiple services need, and Sealed Variables for production secrets you don't want anyone reading back from the dashboard.

Step-by-Step Setup

1

Open your project

Go to railway.app/dashboard and click on your project.

2

Select your service

Click on the service you want to configure (e.g., your web app, API, or database).

3

Click the Variables tab in the service panel.

4

Add variables

You have two options:

  • Add individually: Click "New Variable" and enter key/value
  • Raw Editor: Click "RAW Editor" to paste multiple variables at once
# In Raw Editor, paste like a .env file:
DATABASE_URL=postgresql://...
STRIPE_SECRET_KEY=sk_test_xxxxx
OPENAI_API_KEY=sk-xxxxx
5

Save and deploy

Railway automatically triggers a redeploy when you save variables. You'll see the deployment start in the Deployments tab.

Railway-Specific Features

Every Variable Railway Sets For You

Railway injects these into every service. You don't define them, you can't overwrite them from the dashboard, and you can reference them from your own variables with ${{...}}.

VariableWhat it holds
RAILWAY_PUBLIC_DOMAINThe public service or customer domain, like your-app.up.railway.app
RAILWAY_PRIVATE_DOMAINThe private DNS name other services in the project reach you on
RAILWAY_PROJECT_NAME / RAILWAY_PROJECT_IDThe project the service belongs to
RAILWAY_ENVIRONMENT_NAME / RAILWAY_ENVIRONMENT_IDThe environment this instance is running in
RAILWAY_SERVICE_NAME / RAILWAY_SERVICE_IDThe service itself
RAILWAY_DEPLOYMENT_IDThe current deployment
RAILWAY_SNAPSHOT_IDThe snapshot backing that deployment
RAILWAY_REPLICA_ID / RAILWAY_REPLICA_REGIONWhich replica this is, and where it runs (us-west2, for example)
RAILWAY_VOLUME_NAME / RAILWAY_VOLUME_MOUNT_PATHThe attached volume, if the service has one
RAILWAY_TCP_PROXY_DOMAIN / RAILWAY_TCP_PROXY_PORT / RAILWAY_TCP_APPLICATION_PORTTCP proxy details, if the service has one

Six more appear only when GitHub triggered the deploy: RAILWAY_GIT_COMMIT_SHA, RAILWAY_GIT_BRANCH, RAILWAY_GIT_AUTHOR, RAILWAY_GIT_COMMIT_MESSAGE, RAILWAY_GIT_REPO_NAME and RAILWAY_GIT_REPO_OWNER. Railway's reference page spends one line on that condition: "These variables are provided if the deploy originated from a GitHub trigger." It costs people more than a line, which is the next section.

RAILWAY_PUBLIC_DOMAIN is the one most app code wants. Build absolute URLs from it instead of hardcoding your domain:

# In your service's variable panel, reference the built-in to build a full URL
WEBHOOK_URL=https://${{RAILWAY_PUBLIC_DOMAIN}}/webhooks/stripe
OAUTH_CALLBACK=https://${{RAILWAY_PUBLIC_DOMAIN}}/auth/callback

Built-in variables are read-only. You cannot override RAILWAY_PUBLIC_DOMAIN from the dashboard. If you need a custom domain, set a separate variable like APP_BASE_URL=https://yourdomain.com and use that in your code instead.

For the full ${{...}} reference syntax, and for the way a built-in sitting behind a VITE_ prefix ends up readable in your client bundle, see Railway Reference Variables and RAILWAY_PUBLIC_DOMAIN.

Why RAILWAY_GIT_COMMIT_SHA Comes Back Empty

The git variables exist only when a GitHub push triggered the deploy. Ship the same code with railway up from your laptop, or point the service at a container image, and every RAILWAY_GIT_* value is simply absent. Nothing errors. The variable is just not there.

That bites because release tracking is usually wired straight to it:

// undefined on any CLI deploy or image deploy
Sentry.init({ release: process.env.RAILWAY_GIT_COMMIT_SHA });

Fall back to something that exists on every deployment:

const release =
  process.env.RAILWAY_GIT_COMMIT_SHA || process.env.RAILWAY_DEPLOYMENT_ID;

RAILWAY_DEPLOYMENT_ID is set no matter where the deploy came from, so your errors stay groupable even when there's no commit hash to name them after.

PORT: Injected, But Missing From The Reference

Search Railway's variables reference for PORT and you get RAILWAY_TCP_PROXY_PORT and RAILWAY_TCP_APPLICATION_PORT. Plain PORT is not on that page at all.

It's real anyway. Railway's own "Application failed to respond" doc is where it's documented: your web server "should bind to the host 0.0.0.0 and listen on the port specified by the PORT environment variable, which Railway automatically injects into your application."

So the variable exists, the reference page doesn't list it, and founders reasonably conclude they're supposed to set it themselves. Read it instead:

// Express, Fastify, Nest: same shape
const port = process.env.PORT || 3000;
app.listen(port, "0.0.0.0");

Two ways this goes wrong on a first Railway deploy, and both surface as the same 502:

  • Binding to localhost. app.listen(port, "127.0.0.1") accepts connections only from inside the container. Railway's edge proxy is outside it.
  • A target port that doesn't match. If your public domain has a target port set, it has to be the port the app actually listens on. Railway's troubleshooting page uses exactly this example: a domain configured for port 3000 while the app was listening on 8080.

Variable References

Railway lets you reference variables from other services using the ${{SERVICE.VAR}} syntax:

# Reference the database URL from your Postgres service
DATABASE_URL=${{Postgres.DATABASE_URL}}

# Reference another service's variable
API_URL=${{api.RAILWAY_PUBLIC_DOMAIN}}

Postgres DATABASE_URL

When you add a Railway Postgres service to your project, Railway automatically injects DATABASE_URL and DATABASE_PUBLIC_URL into that database service. To use them in your app service, reference them explicitly:

# In your app service's Variables tab:
DATABASE_URL=${{Postgres.DATABASE_URL}}

If you don't add the reference, your app won't see the variable. Railway doesn't auto-link database variables to other services. The ${{Postgres.DATABASE_URL}} reference is the manual wire you need to add.

Shared Variables

For variables needed across multiple services:

  1. Click "Shared Variables" at the project level
  2. Add your variable (e.g., STRIPE_SECRET_KEY)
  3. In each service, reference it: STRIPE_SECRET_KEY=${{shared.STRIPE_SECRET_KEY}}

Pro tip: Use Shared Variables for API keys that multiple services need. You update the key in one place when rotating, and every service that references it picks up the new value on its next deploy.

Linking is manual and per-service, and a linked variable is present during the build as well as at runtime. Railway Shared Variables: How They Work and What They Expose walks through what that means for an npm install running third-party lifecycle scripts.

Sealed Variables

Sealed Variables hide a value after you save it. The dashboard shows •••••• instead of the secret, builds and runtime still get the real value, and your teammates can't accidentally screenshot a Stripe live key into a Slack thread.

Use Sealed for: production database passwords, Stripe live keys, OAuth client secrets, third-party API keys with billing attached.

Sealed values can't be read back from the UI or CLI once saved. If you forget the value, you rotate the upstream secret and re-enter it. Sealing is also narrower than it looks: Railway Secrets: How Sealed Variables Work and What They Miss lists the five places a sealed value still shows up.

PR (Pull Request) Environments

Railway can spin up an ephemeral environment for every open PR, isolated from production. The variables work in three layers:

  1. Inherited: Variables from the base environment (usually production or staging) carry into the PR env.
  2. Overridden: Set a variable in the PR environment to override the inherited value (e.g., a staging Stripe test key, a separate Postgres database URL).
  3. Built-in PR vars: Railway exposes RAILWAY_GIT_BRANCH, RAILWAY_GIT_COMMIT_SHA, and RAILWAY_GIT_AUTHOR so the preview can show a "Preview for branch X" banner.

Don't inherit production secrets into PR environments. A PR env is reachable on a public URL by default. If you inherit STRIPE_SECRET_KEY=sk_live_..., anyone with the preview URL can hit your live Stripe account. Override with the test key in the PR environment, or scope the secret to production-only.

For the full list of what a preview deploy copies from your base environment (and what it doesn't), see Railway PR Environments: What They Copy From Production. For what an environment isolates in the first place, see Railway Environments: What Is Isolated and What Is Not.

Using the Railway CLI

# Link to your project
railway link

# List variables for current service
railway variables

# Add a variable
railway variables set STRIPE_SECRET_KEY=sk_test_xxxxx

# Get a specific variable
railway variables get DATABASE_URL

# Delete a variable
railway variables delete OLD_KEY

# Run local dev with Railway variables
railway run npm run dev

Common Variable Patterns

# Database (auto-populated when adding Railway Postgres)
DATABASE_URL=${{Postgres.DATABASE_URL}}

# Redis (auto-populated when adding Railway Redis)
REDIS_URL=${{Redis.REDIS_URL}}

# Internal service communication
API_INTERNAL_URL=http://${{api.RAILWAY_PRIVATE_DOMAIN}}:3000

# External URL for webhooks
WEBHOOK_URL=https://${{RAILWAY_PUBLIC_DOMAIN}}/api/webhook

# Secrets (use Shared Variables)
STRIPE_SECRET_KEY=${{shared.STRIPE_SECRET_KEY}}
OPENAI_API_KEY=${{shared.OPENAI_API_KEY}}

Private vs Public Domains: RAILWAY_PRIVATE_DOMAIN is for internal service-to-service communication (faster, no public internet). RAILWAY_PUBLIC_DOMAIN is your externally reachable URL. Use private domains when one service inside Railway talks to another, since traffic stays on the internal network and skips egress charges.

How do I add environment variables in Railway?

Open your project at railway.app/dashboard, click the service you want to configure, open the Variables tab, and click New Variable to enter a key and value. Railway redeploys the service automatically when you save. To paste many at once, use Raw Editor and drop in .env-formatted lines.

What's the difference between Railway shared and service variables?

Service variables live on a single service and are scoped to that service only. Shared Variables sit at the project level and are referenced from each service via ${{shared.NAME}}. Use shared for values multiple services need (like a Stripe secret) so you only rotate them in one place. Use service variables for things only one service should ever see.

How do secrets work in Railway?

Every variable in Railway is encrypted at rest and only injected into builds and runtime processes you authorize. Mark variables as Sealed to hide their value from the dashboard after initial entry (you can still reference them, but you can't read them back). Never put a secret in a build-time NEXT_PUBLIC_ or VITE_ variable, since those get baked into the client bundle.

Do I need to restart after changing variables?

No. Railway automatically triggers a new deployment when you save a variable change. The new deployment runs with the updated values; the old deployment stays live until the new one passes health checks.

How do I use Railway variables locally?

Run railway link once to connect your local directory, then prefix any command with railway run to inject the linked service's variables. Example: railway run npm run dev. This avoids copying secrets into a local .env file.

What is RAILWAY_PUBLIC_DOMAIN and how do I use it?

RAILWAY_PUBLIC_DOMAIN is a built-in variable Railway injects automatically. It holds your service's public URL (e.g., your-app.up.railway.app). Reference it in other variables to build full URLs without hardcoding: WEBHOOK_URL=https://${{RAILWAY_PUBLIC_DOMAIN}}/webhooks/stripe. You cannot override it from the dashboard. If you use a custom domain, create a separate variable like APP_BASE_URL=https://yourdomain.com and reference that instead.

Why is RAILWAY_GIT_COMMIT_SHA empty?

Only a deploy triggered by a GitHub push gets the git variables. If you shipped with railway up from your laptop, or the service runs from a container image, RAILWAY_GIT_COMMIT_SHA, RAILWAY_GIT_BRANCH and the rest are absent rather than empty strings. Fall back to RAILWAY_DEPLOYMENT_ID, which Railway sets on every deployment regardless of where it came from.

Does Railway set the PORT environment variable?

Yes. Railway injects PORT into your application, even though it is not listed on the variables reference page. Read it with process.env.PORT and bind your server to 0.0.0.0, not 127.0.0.1. If your public domain has a target port set, it has to match the port your app actually listens on, otherwise the edge proxy returns a 502.

Find leaked secrets, NEXT_PUBLIC_ slips, and exposed admin paths in 2 minutes. Free.

How-To Guides

Railway Environment Variables: Complete Setup Guide (2026)