@tachyon-gg/railway-deploy
v0.3.1
Published
[](https://github.com/tachyon-gg/railway-deploy/actions/workflows/ci.yml) [
npx @tachyon-gg/railway-deploy project.yaml -e production
# Apply changes
npx @tachyon-gg/railway-deploy --apply -e production project.yamlCLI flags
| Flag | Description |
|------|-------------|
| -e, --environment <name> | Target environment (required except for --validate) |
| --apply | Execute changes (default: dry-run) |
| --stage | Stage changes in Railway without committing (preview in dashboard) |
| -y, --yes | Skip confirmation for destructive ops |
| --allow-data-loss | Allow operations that can cause data loss (e.g., volume deletion) |
| --env-file <path> | Load .env file for ${VAR} resolution |
| -v, --verbose | Show detailed diffs (old -> new values) |
| --no-color | Disable ANSI color output |
| --validate | Validate config without connecting to Railway |
Environment variables
| Variable | Description |
|----------|-------------|
| RAILWAY_TOKEN | Railway API token (required for all API operations) |
Config reference
Project configs are YAML files describing the desired state of a Railway project across one or more environments. Add schema support to your editor:
# yaml-language-server: $schema=./schemas/project.schema.jsonTop-level structure
project: My Project # Railway project name (must match exactly)
environments: # Environments to manage
- staging
- production
shared_variables: { ... } # Variables shared across all services
services: { ... } # Service definitions
volumes: { ... } # Persistent volume definitions
buckets: { ... } # S3-compatible bucket definitionsShared variables
Shared variables are available to all services in an environment. Use the string shorthand for values that are the same everywhere, or the object form for per-environment overrides:
shared_variables:
# String shorthand — same value in all environments
ADMIN_PORT: "8081"
PUBLIC_PORT: "8080"
# Object form — default value with per-environment overrides
JWT_SECRET:
value: ${JWT_SECRET_DEFAULT}
environments:
staging:
value: ${JWT_SECRET_STAGING}
production:
value: ${JWT_SECRET_PROD}Supports ${ENV_VAR} syntax (resolved from your local environment or --env-file) and ${{shared.OTHER_VAR}} self-references.
Note: Shared variables cannot contain
${{service.VAR}}cross-service references. Railway resolves shared variables without a service context.
Volumes
Volumes are declared at the top level with optional per-environment overrides. Services reference them by name.
volumes:
pg-data:
size_mb: 50000
region: us-east4
environments:
production:
size_mb: 100000
redis-data: {} # Minimal declaration — Railway defaults
services:
postgres:
source:
image: postgres:17
volume: # Reference a declared volume
name: pg-data
mount: /var/lib/postgresql/dataEvery volume referenced by a service must be declared in the volumes block.
Buckets
S3-compatible Railway buckets. The key is the bucket name.
buckets:
media-uploads:
region: iad
environments:
eu-production:
region: fra
logs: {} # Minimal — uses default regionServices
Each service defines defaults that apply to all environments. Per-environment overrides go under environments.<name>:
services:
web:
source:
repo: myorg/web-app
start_command: npm start
variables:
PORT: "3000"
environments:
staging:
source:
repo: myorg/web-app
branch: develop
production:
source:
repo: myorg/web-app
branch: main
wait_for_ci: true
# Service without environments block — exists in all environments
redis:
source:
image: redis:7
# Service scoped to specific environments
debug-tools:
source:
image: debug:latest
environments:
staging: {} # Only in stagingService scope rules
- Service has
environmentsblock -> only exists in environments listed there - Service has no
environmentsblock -> exists in ALL declared environments
Merge rules
When a service has per-environment overrides:
| Field type | Merge behavior |
|------------|---------------|
| params, variables | Shallow merge (override keys replace defaults) |
| domains, source, volume, regions, healthcheck, build | Replace entirely |
| Scalar fields (start_command, etc.) | Override replaces |
Service fields reference
Every field below can be used on service defaults, per-environment overrides, and templates.
Source
Source is a discriminated union — use either repo or image, not both.
# Repo source — deploy from a GitHub repository
source:
repo: myorg/my-repo
branch: main # Branch to deploy from
root_directory: /packages/api # Root directory (monorepo support)
wait_for_ci: true # Wait for GitHub Actions to pass before deploying# Image source — deploy from a container image
source:
image: nginx:latest # Docker image (Docker Hub, GHCR, etc.)
registry_credentials: # For private container registries
username: ${REGISTRY_USER}
password: ${REGISTRY_PASS}
auto_updates: # Auto-update schedule for image-based services
monday:
start_hour: 0
end_hour: 6
friday:
start_hour: 0
end_hour: 6Build
Build is a discriminated union — fields depend on the builder value.
# Railpack (default)
build:
builder: railpack
command: npm run build # Custom build command
watch_patterns: # File patterns that trigger deploys
- /packages/api/src/**
metal: true # Enable Metal build environment (faster builds)# Nixpacks
build:
builder: nixpacks
command: npm run build
watch_patterns:
- /packages/api/src/**
metal: true# Dockerfile
build:
builder: dockerfile
dockerfile_path: Dockerfile.prod # Path to Dockerfile
watch_patterns:
- /packages/api/src/**
metal: truerailway_config_file is a separate service-level field (not part of build):
railway_config_file: railway.toml # Path to railway.json/toml in the repositoryDeploy
start_command: npm start # Custom start command
pre_deploy_command: # Run before deployment (e.g., migrations)
- npm run migrate
- npm run seed
cron_schedule: "*/5 * * * *" # Cron schedule (5-field format)
# Note: cron forces restart_policy to NEVER
# and disables serverless
healthcheck: # HTTP healthcheck
path: /health
timeout: 300 # Timeout in seconds (default: 300)
# Restart policy — string shorthand or object with max_retries
restart_policy: always # always, never, or on_failure
restart_policy: # Object form for on_failure with retries
type: on_failure
max_retries: 5
serverless: true # Enable serverless sleeping (scale to zero when idle)
draining_seconds: 30 # Graceful shutdown timeout (SIGTERM to SIGKILL)
overlap_seconds: 10 # Blue-green deploy overlap durationNetworking
# Custom domains
domains:
- app.example.com # Simple domain
- domain: api.example.com # Domain with target port
target_port: 8080
# Railway-provided domain (*.up.railway.app)
railway_domain:
target_port: 3000
# TCP proxy (for non-HTTP services like databases)
tcp_proxy: 5432
# Private networking
private_hostname: postgres # Internal DNS hostname for service-to-service communication
# Outbound networking
ipv6_egress: true # Enable IPv6 outbound traffic
static_outbound_ips: true # Assign permanent outbound IP addressesScaling
regions: us-east4 # Single region (1 replica)
# or
regions: # Multi-region with replica counts
us-east4: 3
us-west1: 1
limits: # Resource limits per replica
memory_gb: 8
vcpus: 4Storage
volume: # Reference a top-level volume
name: pg-data # Must match a key in the volumes block
mount: /var/lib/postgresql/data # Absolute mount pathVariables
variables:
PORT: "3000"
DATABASE_URL: ${{Postgres.DATABASE_URL}} # Railway runtime reference
API_KEY: ${LOCAL_API_KEY} # Resolved from local env at config time
OLD_VAR: null # Marks for deletionVariable syntax
| Syntax | Resolved | Description |
|--------|----------|-------------|
| ${ENV_VAR} | At config load time | Reads from local environment (or --env-file) |
| ${{service.VAR}} | At Railway runtime | Railway reference variable (cross-service) |
| %{param} | At config load time | Template parameter substitution |
| %{service_name} | At config load time | Built-in: the service's config key |
| null | N/A | Marks a variable for deletion |
%{param} is expanded first, so it can be used inside ${{}} Railway references:
variables:
DATABASE_URL: ${{%{service_name}.DATABASE_URL}}
REDIS_URL: ${{%{cache_service}.REDIS_URL}}Service templates
Templates extract reusable service definitions with parameterized values. The built-in %{service_name} param resolves to the service's key in the config.
# services/web.yaml
params:
tag:
required: true
replicas:
default: "1"
source:
image: ghcr.io/org/app:%{tag}
variables:
APP_VERSION: "%{tag}"
SERVICE_NAME: "%{service_name}"
DATABASE_URL: ${{Postgres.DATABASE_URL}}
domains:
- "%{service_name}.example.com"
healthcheck:
path: /health
timeout: 300
regions: us-east4Referenced from a project config:
services:
web:
template: services/web.yaml
params:
replicas: "1"
environments:
staging:
params:
tag: alpha
production:
params:
tag: v2.0.0
replicas: "3"
variables:
EXTRA: added-by-env
APP_VERSION: null # Deletes the template-defined variable
domains:
- production.example.com # Overrides template domainsComplete example
# yaml-language-server: $schema=./schemas/project.schema.json
project: My SaaS App
environments:
- staging
- production
shared_variables:
APP_PORT: "3000"
SENTRY_DSN:
value: ${SENTRY_DSN_DEFAULT}
environments:
production:
value: ${SENTRY_DSN_PROD}
volumes:
pg-data:
size_mb: 50000
environments:
production:
size_mb: 200000
redis-data: {}
buckets:
uploads:
region: iad
services:
web:
source:
repo: myorg/web-app
root_directory: /packages/web
build:
builder: nixpacks
command: npm run build
metal: true
start_command: npm start
pre_deploy_command: npm run migrate
healthcheck:
path: /health
timeout: 60
restart_policy:
type: on_failure
max_retries: 5
serverless: true
railway_domain:
target_port: 3000
variables:
PORT: "3000"
DATABASE_URL: ${{Postgres.DATABASE_URL}}
environments:
staging:
source:
repo: myorg/web-app
branch: develop
domains:
- staging.example.com
production:
source:
repo: myorg/web-app
branch: main
wait_for_ci: true
domains:
- app.example.com
- domain: api.example.com
target_port: 8080
regions:
us-east4: 2
limits:
memory_gb: 4
vcpus: 2
postgres:
source:
image: postgres:17
private_hostname: postgres
volume:
name: pg-data
mount: /var/lib/postgresql/data
tcp_proxy: 5432
variables:
POSTGRES_DB: myapp
redis:
source:
image: redis:7-alpine
private_hostname: redis
volume:
name: redis-data
mount: /data
tcp_proxy: 6379
worker:
template: services/worker.yaml
params:
queue: default
serverless: false
cron:
source:
repo: myorg/web-app
root_directory: /packages/cron
cron_schedule: "0 0 * * *"
start_command: node scripts/cleanup.jsKnown limitations
- Region management. Setting
regionsdeploys to those regions. Railway always maintains at least one region — the last region cannot be removed. Changing regions is supported (old regions are removed and new ones added). Multi-region is supported via a map of region to replica count. - Service groups are read-only. Railway's public API does not expose group creation -- groups can only be managed via the Railway dashboard. Existing groups are respected when reading config.
- Custom domains may require DNS verification to take effect.
- Registry credentials are write-only. Railway never returns credentials in config responses, so removal of registry credentials from your config is not detectable -- we simply stop sending them.
- Static outbound IPs are managed via a separate API call (not atomic with the config patch). If the patch succeeds but the egress call fails, IPs may not be configured.
- Volume size/region can only be set or increased, not cleared or reduced. Railway does not support shrinking volumes.
- Volume mount removal is supported via the
volumeDeletemutation and requires the--allow-data-lossflag, since it permanently deletes the volume and its data. - Bucket deletion is not supported by Railway's API. Buckets that are removed from config will be left in place with a warning.
JSON schemas
Editor support (autocompletion, validation) is available via JSON schemas:
schemas/project.schema.json-- project config filesschemas/service-template.schema.json-- service template files
Development
bun install # Install dependencies
bun run test # Run unit tests
bun run test:integration # Run integration tests (requires RAILWAY_TOKEN)
bun run typecheck # Type check
bun run lint # Lint (Biome)
bun run lint:fix # Auto-fix lint issues
bun run codegen # Regenerate GraphQL types
bun run build # Build for distribution