Configure
Manage environments, variables, service config, domains, and networking.
Environments
List and switch
railway environment list --json
railway environment list --ephemeral --json # only PR environments
railway environment list --no-ephemeral --json # hide PR environments
railway environment link <environment> # switch active environmentCreate
railway environment new <name>
railway environment new <name> --duplicate <source-environment> # clone config from existingDuplicating copies all service configurations and variables from the source environment.
Variables
Read, set, and delete
railway variable list --service <service> --environment <env> --json
railway variable set KEY=value --service <service> --environment <env>
railway variable delete KEY --service <service> --environment <env>Variable changes trigger a redeployment by default. This is usually the desired behavior, since the service picks up the values on restart. Use --skip-deploys only when you plan to redeploy or restart separately.
CLI 5.34.2+ accepts an empty assignment such as railway variable set OPTIONAL_VALUE= --service <service>. Empty is a value, not a deletion. Before an idempotent delete, list keys and delete only if present.
Bulk edit with a reviewed diff
CLI 5.48+ opens an editor and presents the changes before applying:
railway variable edit --project <project-id> --environment <env> --service <service>
railway variable edit --demo # offline fixture; no Railway mutationThis workflow requires a TTY or an explicitly configured $EDITOR/$VISUAL. Prefer set/delete for deterministic agent edits when no editor workflow was requested. Saving the editor is not approval: the CLI shows a redacted diff and asks before applying. Noninteractive apply requires --yes; deletions in agent or noninteractive sessions also require --confirm-destructive, within the user's approved scope.
Removing a line deletes that variable. Keep <sealed> placeholders unchanged to preserve sealed values. Railway-provided variables are comments and cannot be edited. --reveal exposes plaintext in the diff; use only when intended. --skip-deploys commits changes without triggering deploys.
Set sensitive values
Use stdin for secrets or values that shouldn't appear in shell history:
printf "%s" "$SECRET_VALUE" | railway variable set API_KEY --stdin --service <service>
railway variable set API_URL=https://api.example.com --project <project-id> --environment <env> --service <service>Template syntax
Railway supports interpolation between services and shared variables:
${{KEY}} # same-service variable
${{shared.API_KEY}} # shared variable
${{postgres.DATABASE_URL}} # variable from another service
${{api.RAILWAY_PRIVATE_DOMAIN}} # another service's private domainWiring example, a frontend connecting to a backend over private networking:
BACKEND_URL=http://${{api.RAILWAY_PRIVATE_DOMAIN}}:${{api.PORT}}Wiring services together
Each managed database creates connection variables automatically. Reference them from other services using template syntax:
| Database | Variable reference |
|---|---|
| Postgres | ${{Postgres.DATABASE_URL}} |
| Redis | ${{Redis.REDIS_URL}} |
| MySQL | ${{MySQL.MYSQL_URL}} |
| MongoDB | ${{MongoDB.MONGO_URL}} |
Service names in references are case-sensitive and must match the service name exactly as it appears in the project.
Public vs private networking decision:
| Traffic path | Use |
|---|---|
| Browser → API | Public domain |
| Service → Service | Private domain (RAILWAY_PRIVATE_DOMAIN) |
| Service → Database | Private (automatic, uses internal DNS) |
Frontend apps cannot use private networking. Frontends run in the user's browser, not on Railway's network. They cannot reach RAILWAY_PRIVATE_DOMAIN or internal database URLs. Options:
- Backend proxy (recommended): frontend calls a backend API on a public domain, backend connects to the database over the private network.
- Public database URL: use the public connection variable (for example,
${{Postgres.DATABASE_PUBLIC_URL}}). This requires a TCP proxy on the database service and exposes the database to the internet. Use this only for development or low-sensitivity data.
Railway-provided variables
These are set automatically at runtime. Availability depends on resource configuration.
Networking:
| Variable | Available when |
|---|---|
RAILWAY_PUBLIC_DOMAIN |
Public domain is configured |
RAILWAY_PRIVATE_DOMAIN |
Always (internal DNS for service-to-service traffic) |
RAILWAY_TCP_PROXY_DOMAIN |
TCP proxy is enabled |
RAILWAY_TCP_PROXY_PORT |
TCP proxy is enabled |
Context:
| Variable | Available when |
|---|---|
RAILWAY_PROJECT_ID |
Always |
RAILWAY_ENVIRONMENT_ID |
Always |
RAILWAY_ENVIRONMENT_NAME |
Always |
RAILWAY_SERVICE_ID |
Always |
RAILWAY_SERVICE_NAME |
Always |
RAILWAY_DEPLOYMENT_ID |
Always |
RAILWAY_REPLICA_ID |
Replicas configured |
RAILWAY_REPLICA_REGION |
Multi-region configured |
Git (present when deployed from a linked repo):
| Variable | Description |
|---|---|
RAILWAY_GIT_COMMIT_SHA |
Full commit hash of the deployed revision |
RAILWAY_GIT_AUTHOR |
Commit author name |
RAILWAY_GIT_COMMIT_MESSAGE |
First line of the commit message |
RAILWAY_GIT_BRANCH |
Branch that triggered the deploy |
Storage (present when a volume is attached):
| Variable | Description |
|---|---|
RAILWAY_VOLUME_MOUNT_PATH |
Filesystem path where the volume is mounted |
RAILWAY_VOLUME_NAME |
Name of the attached volume |
Sealed variables are write-only. CLI 5.47.2+ lists their names with null in JSON, <sealed> in the table, or a comment in KV output. A null value means the sealed key already exists; do not recreate or clear it as though it were missing. Ordinary variable output can contain plaintext secrets.
Service config
Service configuration controls source, build, deploy, and networking settings. There are two ways to mutate it.
Dot-path patch
For single-field changes:
railway environment edit --service-config <service> deploy.startCommand "npm start"
railway environment edit --service-config <service> build.buildCommand "npm run build"
railway environment edit --service-config <service> source.rootDirectory "/apps/api"
railway environment edit --service-config <service> deploy.numReplicas 2
railway environment edit --project <project-id> --environment production --service-config <service> deploy.startCommand "npm start"JSON patch
For multi-field changes or complex structures:
railway environment edit --json <<'JSON'
{"services":{"<service-id>":{"build":{"buildCommand":"npm run build"},"deploy":{"startCommand":"npm start"}}}}
JSONResolve exact service IDs from railway service list --json before constructing JSON patches. Using names in the JSON payload doesn't work.
Stage config changes
Stage changes when the user wants to review config before committing it:
railway environment edit --service-config <service> build.buildCommand "npm run build" --stage
railway environment edit --service-config <service> deploy.startCommand "npm start" --message "Set production start command"Use --stage only when the user requests staged config changes. Use regular edits for immediate mutations.
Config schema (typed paths)
Include only keys you're changing. The full shape:
Source: source.image (string), source.repo (string), source.branch (string), source.rootDirectory (string), source.checkSuites (boolean), source.commitSha (string), source.autoUpdates.type (string: disabled, patch, minor)
Build: build.builder (string: RAILPACK, NIXPACKS, DOCKERFILE), build.buildCommand (string), build.dockerfilePath (string), build.watchPatterns (string array), build.nixpacksConfigPath (string)
Deploy: deploy.startCommand (string), deploy.preDeployCommand (string), deploy.healthcheckPath (string), deploy.healthcheckTimeout (integer), deploy.numReplicas (integer), deploy.restartPolicyType (string: ON_FAILURE, ALWAYS, NEVER), deploy.restartPolicyMaxRetries (integer), deploy.sleepApplication (boolean), deploy.cronSchedule (string), deploy.multiRegionConfig (object)
Multi-region config structure for deploy.multiRegionConfig:
{ "us-west2": { "numReplicas": 2 }, "europe-west4-drams3a": { "numReplicas": 1 } }| Region identifier | Location |
|---|---|
us-west2 |
US West (Oregon) |
us-east4-eqdc4a |
US East (Virginia) |
europe-west4-drams3a |
Europe (Netherlands) |
asia-southeast1-eqsg3a |
Asia (Singapore) |
Natural language mapping: "add replicas in Europe" → europe-west4-drams3a, "US East" → us-east4-eqdc4a. When the user doesn't specify a region, query current config first with railway environment config --json to see existing region assignments before modifying.
Variables: variables.<KEY>.value (string), variables.<KEY>.isOptional (boolean), variables.<KEY>.isSealed (boolean). Delete a variable by setting it to null.
Lifecycle: isDeleted (boolean) removes the service. isCreated (boolean) marks as new. Prefer railway service delete for normal service deletion.
Storage: volumeMounts.<volume-id>.mountPath (string), volumes.<volume-id>.isDeleted (boolean)
Buckets: buckets.<bucket-id>.region (string: sjc, iad, ams, sin), buckets.<bucket-id>.isCreated (boolean), buckets.<bucket-id>.isDeleted (boolean). Buckets are created at the project level via railway bucket create and deployed to environments via config patches. The CLI handles this automatically, so use railway bucket commands
Shared variables and project-level config
railway environment edit --json <<'JSON'
{"sharedVariables":{"API_BASE":{"value":"https://example.com"}}}
JSONShared variables are accessible from any service via ${{shared.KEY}}.
Read config
Always inspect before mutating. Config patches merge, so you need to know the state to avoid overwriting fields unintentionally:
railway environment config --jsonVerify after mutation to confirm the change took effect:
railway environment config --json
railway service list --jsonDomains
Use the railway domain command for domain lifecycle work. Avoid raw environment edit JSON patches for normal domain management.
Create domains
railway domain --service <service> --json # generate a Railway domain
railway domain example.com --service <service> --json # add a custom domain
railway domain example.com --service <service> --port 8080 --jsonOne Railway-provided domain is allowed per service. Multiple custom domains are supported. Custom domain creation returns the DNS records to add at the DNS provider. Add both the routing record and ownership verification record exactly as returned.
Requests to a custom domain can return 404 until Railway verifies the ownership TXT record. DNS can take up to 72 hours to propagate.
Inspect and update domains
railway domain list --service <service> --json
railway domain status example.com --service <service> --json
railway domain update example.com --port 8080 --service <service> --json
railway domain update old-name.up.railway.app --domain new-name --service <service> --json
railway domain certificate retry example.com --service <service> --jsonUse status when DNS or certificate issuance is not healthy. Retry certificate issuance only after fixing DNS.
Delete domains
railway domain delete example.com --service <service> --yes --jsonDomain deletion is destructive. Confirm the domain and service before running it.
Webhooks
Project webhooks POST a JSON payload to a URL whenever a chosen deployment, monitor, or volume-alert event happens anywhere in the project. They are project-scoped, not per service or environment, and are managed with five MCP tools. There is no railway webhook CLI command. Resolve the project ID from railway status --json or list-projects first.
| Tool | Purpose |
|---|---|
list-webhooks |
Every webhook in the project: URL, eventTypes, includePreviewEnvironments, the names of its custom headers (headerNames), and the id the other tools take |
create-webhook |
url plus optional eventTypes (defaults to Deployment.failed, Deployment.crashed, Deployment.oom_killed), includePreviewEnvironments (defaults to true), and headers |
update-webhook |
webhookId plus any of url, eventTypes, includePreviewEnvironments, headers. Only the fields passed change; eventTypes and headers each replace the whole set |
test-webhook |
POST a sample event and report the HTTP status. Pass url (and optional headers) to try a new endpoint, or webhookId to send to an existing webhook with its stored URL and headers. Nothing is stored |
delete-webhook |
Remove a webhook by webhookId. Destructive; confirm the URL with the user first |
Discord and Slack incoming-webhook URLs are detected and receive a formatted message instead of the raw JSON. Run test-webhook before create-webhook: Railway treats anything outside 2xx/3xx as a failed delivery, and a dead endpoint is paused for 24 hours after repeated failures.
Create webhook for project <project-id>: url https://hooks.example.com/railway, eventTypes Deployment.failed Deployment.crashed, headers { "Authorization": "Bearer <token>" }Custom headers
A webhook can carry up to 20 custom HTTP headers, sent with every delivery and with test-webhook. Use them for whatever the receiver needs to trust or route the request: an Authorization bearer token, an API key, a shared secret the receiver compares, a routing header. This replaces the old advice of hiding a secret in the URL, which still works but leaks into logs more easily.
- Values are write-only. They are encrypted at rest and never returned:
list-webhooksand every other tool's output carryheaderNamesonly. Keep the value in the user's secret store; Railway cannot show it again. create-webhookandtest-webhooktakeheadersas a name-to-value map.update-webhookreplaces the whole set. Every header not listed is removed and{}clears them all. Anullvalue keeps the stored value for that name, so a secret survives a change without being resent:{ "Authorization": null, "X-Env": "prod" }keeps the token and setsX-Env. A URL-only update carries the stored headers forward automatically.test-webhookwithwebhookIdsends the stored headers; anyheaderspassed alongside override the stored one of the same name and the rest are still sent. Headers are validated before the request goes out, so a bad name gets a clear error instead of a0status.- Name rules. RFC 9110 token characters, up to 128 characters; values up to 4096 characters with no CR or LF.
Host,Content-Type,Content-Lengthand the hop-by-hop headers (Transfer-Encoding,Connection,Keep-Alive,Upgrade,TE,Trailer,Expect) are refused, as is any name starting withproxy-orx-railway-. Two names differing only by case count as a duplicate. The dashboard form applies the same rules inline and disables Save until a bad row is fixed.
Without MCP, the same fields are on the GraphQL API: headers inside the webhook channel config of notificationRuleCreate / notificationRuleUpdate (a null value keeps the stored one there too), and webhookTest(url, payload, headers: [WebhookHeaderInput!], notificationRuleId). Inspect them with railway api describe.
Networking commands
Private networking
For service-to-service traffic within a project, use private domain references instead of public URLs. This avoids egress and is faster:
BACKEND_URL=http://${{api.RAILWAY_PRIVATE_DOMAIN}}:${{api.PORT}}railway private-network status --service <service> --json
railway private-network update api-internal --service <service> --jsonWhen multiple private networks exist, pass --network <name-or-id>. Endpoint updates take the prefix only, without .internal.
TCP proxies
Use TCP proxies to expose non-HTTP ports, such as database or game server ports, to the public internet. Only one TCP proxy is allowed per service instance.
railway tcp-proxy list --service <service> --json
railway tcp-proxy create --service <service> --port 5432 --json
railway tcp-proxy status <proxy-id-or-domain-or-port> --service <service> --json
railway tcp-proxy delete <proxy-id-or-domain-or-port> --service <service> --yes --jsonTCP proxy creation updates service networking config. If the proxy does not become active, redeploy the service and check status.
Outbound networking
Use outbound networking commands for Static Outbound IPs and outbound IPv6:
railway outbound-network status --service <service> --json
railway outbound-network static-ip status --service <service> --json
railway outbound-network static-ip enable --service <service> --json
railway outbound-network static-ip disable --service <service> --json
railway outbound-network ipv6 status --service <service> --json
railway outbound-network ipv6 enable --service <service> --json
railway outbound-network ipv6 disable --service <service> --jsonStatic Outbound IP changes are committed directly but require a redeploy before outbound traffic uses the new assignment. Outbound IPv6 changes are staged as environment config changes; commit staged changes with railway environment edit to trigger the redeploy.
CDN caching
CDN caching is service-scoped and requires an applied public domain.
railway cdn status --service <service> --json
railway cdn enable --service <service> --json
railway cdn update --service <service> --html-caching force --default-ttl 4h --json
railway cdn update --service <service> --no-swr --purge-on-deploy all --json
railway cdn purge html --service <service> --json
railway cdn purge all --service <service> --json
railway cdn disable --service <service> --jsonUse the response headers x-cache and age to verify cache behavior. Cache hits do not reach the service, so service logs and server-side metrics can undercount traffic after caching is enabled.
WAF Under Attack Mode
Under Attack Mode is service-scoped, requires an applied public domain, and is intended for active bot floods or DDoS events.
railway waf under-attack status --service <service> --json
railway waf under-attack enable --service <service> --duration 1h --json
railway waf under-attack enable --service <service> --json
railway waf under-attack disable --service <service> --jsonWhile active, browser visitors must pass a check. Non-browser API clients and webhooks may receive 429 responses, so warn the user before enabling it on API-only domains.
Troubleshoot configuration
- Invalid dot-path: check field names and types in the config schema section above
- Wrong service key in JSON patch: resolve service IDs from
railway service list --json - Variable change didn't take effect: verify with
railway variable list, changes trigger redeploy by default - Domain returns errors: run
railway domain status, verify the target port, then check HTTP logs - DNS propagation delay: custom domains can take up to 72 hours to propagate worldwide
- Cloudflare proxy issues: align SSL/TLS mode per Railway's domain guidance
- Private networking failing: run
railway private-network status, verify the service is listening on the referenced port, and check network flow logs - Outbound allowlist still sees old IPs: redeploy after enabling/disabling Static Outbound IPs
- IPv6 still disabled: commit the staged environment change and wait for redeploy
- CDN appears ineffective: check
x-cache,age, cache headers,Set-Cookie,Authorization, method, and response size - Webhook header rejected: the name is reserved (
Host,Content-Type,Content-Length, hop-by-hop), starts withproxy-orx-railway-, duplicates another name ignoring case, or the value has a newline; rename it or drop it, and stay under 20 headers - Webhook headers disappeared after an update:
update-webhookreplaces the wholeheadersset, so a map without a name removes it; passnullfor names to keep, or omitheadersentirely - WAF breaks API clients: Under Attack Mode blocks non-browser traffic; disable it or scope protection to browser-facing services
- Multi-region patch ignored: verify region names match the exact identifiers (
us-west2,us-east4-eqdc4a,europe-west4-drams3a,asia-southeast1-eqsg3a)
Validated against
- Docs: environment.md, variable.md, domain.md, tcp-proxy.md, private-network.md, outbound-network.md, cdn.md, waf.md, webhooks.md
- Platform source (railwayapp/mono):
packages/backboard/src/handlers/http/routes/mcp/tools/webhooks.ts(webhook MCP tools),packages/backboard/src/controllers/notifications/validation.ts(validateWebhookHeaders),packages/backboard/src/graphql/v2/schema/schema.graphql(webhookTest,WebhookHeaderInput) - CLI source: environment/mod.rs, environment/edit.rs, variable.rs, domain.rs, tcp_proxy.rs, private_network.rs, outbound_networking.rs, cdn.rs, waf.rs