Infrastructure as Code Reference
This page documents the TypeScript authoring API for Railway Infrastructure as Code. TypeScript is generally available. Python (railway_sdk) and Go (github.com/railwayapp/railway-go-sdk) are beta mirrors of the same graph. For the workflow, CLI commands, and Python/Go examples, see Infrastructure as Code.
Authoring
A Railway configuration file exports defineRailway.
import { defineRailway, project, service } from "railway/iac";
export default defineRailway(() => {
const web = service("web");
return project("my-project", {
resources: [web],
});
});When one repository holds every service, this file is the single source for the environment. When services live in separate repositories, each repository's file exports a partial name (Python PARTIAL, Go const Partial) and manages only the resources it declares:
export const partial = "api";The CLI records which partial owns each resource and rejects a file that declares a resource owned by another partial. See Multi-repo projects.
Use railway config partials to list,
release, or transfer ownership without changing the resources. Renaming the
partial export alone doesn't transfer ownership. After changing ownership,
update the authoring configuration and create a fresh plan.
Environment context
defineRailway receives a context object from the CLI. Use it to render different desired state for the Railway environment you are planning or applying to.
export default defineRailway((ctx) => {
const prod = ctx.environment === "production";
const web = service("web", {
replicas: prod ? 3 : 1,
});
return project("my-project", {
resources: [web],
});
});Available context fields include:
| Field | Description |
|---|---|
ctx.command | The command being run, such as plan or apply. |
ctx.projectId | The linked Railway project ID. |
ctx.projectName | The linked Railway project name. |
ctx.environmentId | The target environment ID. |
ctx.environment | The target environment name. |
ctx.environmentName | Alias for the target environment name. |
ctx.isEnvironment(name) | Returns true when the current environment name matches. |
Services
Create a service with service(name, config).
const web = service("web", {
source: github("acme/web"),
build: "pnpm build",
start: "pnpm start",
healthcheck: "/health",
});Service names should match the names users see in Railway. Names with spaces are valid:
const docsFrontend = service("Docs Frontend", {
source: github("acme/docs"),
});Sources
Use a GitHub source:
const web = service("web", {
source: github("owner/repo", { branch: "main" }),
});Use a Docker image:
const worker = service("worker", {
source: image("ghcr.io/acme/worker:latest"),
});Configure automatic image updates with autoUpdates:
const worker = service("worker", {
source: image("ghcr.io/acme/worker:1.2.3", {
autoUpdates: { type: "patch" },
}),
});The type value can be disabled, patch, or minor. Automatic updates are
supported only for Docker Hub and GitHub Container Registry (GHCR) image
sources. They aren't supported for GitHub repository sources or images from
other registries. railway config pull omits stale update policies from those
unsupported sources.
Omit source when .railway/railway.ts should manage service settings but not declare a GitHub repository or Docker image:
const web = service("web", {
build: "bun run build",
start: "bun src/index.ts",
});Build and start commands
const web = service("web", {
build: "pnpm build",
start: "pnpm start",
});Healthchecks
const api = service("api", {
healthcheck: "/health",
healthcheckTimeout: 30,
});Pre-deploy command
Run a command, such as a database migration, between the build and the deploy:
const web = service("web", {
start: "node .output/server/index.mjs",
preDeploy: "npx drizzle-kit migrate",
});The command runs with access to your service variables and the private network, and a failing command stops the deployment. See Pre-deploy command for the runtime behavior.
railway config pull renders this field as deploy: { preDeployCommand: ["..."] }, which is equivalent.
Replicas
Use replicas for scaling intent:
const web = service("web", {
replicas: 3,
});For advanced placement, specify regions:
const web = service("web", {
replicas: {
"us-west2": 2,
"europe-west4": 1,
},
});Tracing
tracing holds the service's tracing switches for the environment. enabled makes the edge trace requests to the service and adds the OTEL_* variables on its next deploy; autoInstrumentation turns on automatic instrumentation, which only takes effect while enabled is true. Railway stores only the switches that are on, so an untraced service has no tracing block and enabled: false is the same as leaving it out. railway config pull renders the block for a traced service:
const api = service("api", {
tracing: { enabled: true, autoInstrumentation: true },
});In a plan, a change to enabled redeploys the service so the variables land. A change to autoInstrumentation alone reaches the running containers without a deploy.
Not yet supported by the SDKs. service() in the TypeScript SDK, and its Python and Go mirrors, don't accept tracing yet, so a tracing block you write is dropped before the CLI sees it. The compiled config then has no block, so railway config plan against a traced service proposes to remove tracing, and railway config apply would turn tracing off. Until the SDKs add the field, set tracing from the Traces page or with railway trace, and don't apply a plan that removes a tracing block you didn't remove yourself.
Environment variables
Set literal variables:
const web = service("web", {
env: {
NODE_ENV: "production",
},
});Reference another service or database:
const db = postgres("postgres");
const web = service("web", {
env: {
DATABASE_URL: db.env.DATABASE_URL,
},
});Reference a shared variable defined on the environment using the context:
export default defineRailway((ctx) => {
const web = service("web", {
env: {
SENTRY_DSN: ctx.shared.SENTRY_DSN,
},
});
return project("my-project", { resources: [web] });
});ctx.shared.NAME compiles to ${{shared.NAME}}. It points at an existing shared variable on the environment; it does not define or manage the shared variable itself.
Preserve an existing Railway-managed value:
const web = service("web", {
env: {
STRIPE_SECRET_KEY: preserve(),
},
});preserve() is mainly used for imported secrets whose values are not available to the CLI. It means “keep the value that is already set in Railway.”
Databases
Railway provides helpers for common databases:
const db = postgres("postgres");
const cache = redis("redis");
const mysqlDb = mysql("mysql");
const mongoDb = mongo("mongo");Reference database variables from services:
const api = service("api", {
env: {
DATABASE_URL: db.env.DATABASE_URL,
REDIS_URL: cache.env.REDIS_URL,
},
});Database provisioning is handled by Railway product workflows. The configuration file describes the database intent; you do not need to attach a volume to managed database helpers yourself.
Volumes
Create a persistent volume with volume(name, config) and attach it to a service with volumeMounts:
const data = volume("backend-data", {
region: "us-west2",
sizeMB: 1024,
});
const backend = service("backend", {
start: "node server.js",
volumeMounts: {
"/data": data,
},
});
return project("my-project", {
resources: [backend, data],
});A volume can be attached to one service. The object key in volumeMounts is the mount path inside the service container, and the value is the volume(...) resource to mount there.
Volume config supports:
| Field | Description |
|---|---|
region | Region where the volume is provisioned. |
sizeMB | Requested volume size in megabytes. |
Increasing sizeMB is planned as a non-destructive resize. Decreasing sizeMB, deleting a volume, detaching a mounted volume, or changing placement can affect persisted data and is treated as destructive.
railway config pull imports existing volumes and service mounts. Managed database volumes may appear in imported configuration so Railway can preserve the existing volume resource, but database helpers such as postgres("postgres") still own the database product intent.
Buckets
Create an object storage bucket:
const media = bucket("media", {
region: "iad",
});Bucket regions cannot be changed after creation. If you need a different region, create a new bucket in that region, copy the data, update your services to use the new bucket, and then remove the old bucket when it is no longer needed.
Custom domains
Attach custom domains to a service:
const web = service("web", {
domains: ["app.example.com"],
});Specify a target port:
const api = service("api", {
domains: [{ domain: "api.example.com", port: 3000 }],
});Generated Railway service domains are not included in .railway/railway.ts.
Groups
Use groups to organize resources on the Railway canvas:
const api = service("api");
const worker = service("worker");
const db = postgres("postgres");
const backend = group("Backend", [api, worker, db]);
return project("my-app", {
resources: [backend],
});Groups are structural. They make large projects easier to scan in both .railway/railway.ts and the Railway canvas.
Larger example
import {
bucket,
defineRailway,
github,
group,
postgres,
preserve,
project,
redis,
service,
volume,
} from "railway/iac";
export default defineRailway((ctx) => {
const prod = ctx.environment === "production";
const db = postgres("postgres");
const cache = redis("redis");
const uploads = bucket("uploads", { region: "iad" });
const workerData = volume("worker-data", {
region: "us-west2",
sizeMB: 1024,
});
const api = service("api", {
source: github("acme/monorepo", { rootDirectory: "apps/api" }),
build: "pnpm --filter api build",
start: "pnpm --filter api start",
healthcheck: "/health",
replicas: prod ? { "us-west2": 2, "europe-west4": 1 } : 1,
env: {
DATABASE_URL: db.env.DATABASE_URL,
REDIS_URL: cache.env.REDIS_URL,
JWT_SECRET: preserve(),
},
});
const web = service("web", {
source: github("acme/monorepo", { rootDirectory: "apps/web" }),
build: "pnpm --filter web build",
start: "pnpm --filter web start",
domains: prod ? ["app.example.com"] : [],
env: {
API_HOST: api.env.RAILWAY_PRIVATE_DOMAIN,
},
});
const worker = service("worker", {
source: github("acme/monorepo", { rootDirectory: "apps/worker" }),
build: "pnpm --filter worker build",
start: "pnpm --filter worker start",
volumeMounts: {
"/data": workerData,
},
env: {
DATABASE_URL: db.env.DATABASE_URL,
REDIS_URL: cache.env.REDIS_URL,
},
});
const backend = group("Backend", [db, cache, api, worker]);
const storage = group("Storage", [uploads, workerData]);
return project("acme", {
resources: [backend, storage, web],
});
});