@tachyon-gg/railway-deploy 0.2.10 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +322 -139
  2. package/dist/index.js +4325 -2207
  3. package/package.json +5 -4
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![Bun](https://img.shields.io/badge/runtime-Bun-f9f1e1)](https://bun.sh/)
8
8
  [![Biome](https://img.shields.io/badge/linter-Biome-60a5fa)](https://biomejs.dev/)
9
9
 
10
- Declarative infrastructure management for [Railway](https://railway.com). Define your Railway project's services, variables, domains, volumes, and buckets in YAML, and `railway-deploy` will diff against the live state and apply changes -- like Terraform, but purpose-built for Railway.
10
+ Declarative infrastructure management for [Railway](https://railway.com). Define your Railway project's services, variables, domains, volumes, and buckets in YAML, and `railway-deploy` will diff against the live state and apply changes atomically -- like Terraform, but purpose-built for Railway.
11
11
 
12
12
  ## Quick start
13
13
 
@@ -16,21 +16,24 @@ Declarative infrastructure management for [Railway](https://railway.com). Define
16
16
  npx @tachyon-gg/railway-deploy --help
17
17
 
18
18
  # Validate a config file
19
- npx @tachyon-gg/railway-deploy --validate environments/production.yaml
19
+ npx @tachyon-gg/railway-deploy --validate project.yaml
20
20
 
21
21
  # Dry-run (show what would change)
22
- npx @tachyon-gg/railway-deploy environments/production.yaml
22
+ npx @tachyon-gg/railway-deploy project.yaml -e production
23
23
 
24
24
  # Apply changes
25
- npx @tachyon-gg/railway-deploy --apply environments/production.yaml
25
+ npx @tachyon-gg/railway-deploy --apply -e production project.yaml
26
26
  ```
27
27
 
28
28
  ## CLI flags
29
29
 
30
30
  | Flag | Description |
31
31
  |------|-------------|
32
+ | `-e, --environment <name>` | Target environment (required except for `--validate`) |
32
33
  | `--apply` | Execute changes (default: dry-run) |
34
+ | `--stage` | Stage changes in Railway without committing (preview in dashboard) |
33
35
  | `-y, --yes` | Skip confirmation for destructive ops |
36
+ | `--allow-data-loss` | Allow operations that can cause data loss (e.g., volume deletion) |
34
37
  | `--env-file <path>` | Load `.env` file for `${VAR}` resolution |
35
38
  | `-v, --verbose` | Show detailed diffs (old -> new values) |
36
39
  | `--no-color` | Disable ANSI color output |
@@ -46,111 +49,244 @@ npx @tachyon-gg/railway-deploy --apply environments/production.yaml
46
49
 
47
50
  ## Config reference
48
51
 
49
- Environment configs are YAML files describing the desired state of a Railway environment. Add schema support to your editor:
52
+ Project configs are YAML files describing the desired state of a Railway project across one or more environments. Add schema support to your editor:
50
53
 
51
54
  ```yaml
52
- # yaml-language-server: $schema=./schemas/environment.schema.json
55
+ # yaml-language-server: $schema=./schemas/project.schema.json
53
56
  ```
54
57
 
55
- ### Top-level fields
58
+ ### Top-level structure
56
59
 
57
60
  ```yaml
58
- project: My Project # Railway project name (must match exactly)
59
- environment: production # Railway environment name
61
+ project: My Project # Railway project name (must match exactly)
62
+ environments: # Environments to manage
63
+ - staging
64
+ - production
65
+
66
+ shared_variables: { ... } # Variables shared across all services
67
+ services: { ... } # Service definitions
68
+ volumes: { ... } # Persistent volume definitions
69
+ buckets: { ... } # S3-compatible bucket definitions
70
+ ```
60
71
 
61
- shared_variables: # Variables shared across all services
62
- APP_ENV: production
63
- API_PORT: "8080"
72
+ ### Shared variables
64
73
 
65
- services: # Map of service name -> config
66
- web: { ... }
67
- worker: { ... }
74
+ 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:
68
75
 
69
- buckets: # S3-compatible buckets
70
- media:
71
- name: media-uploads
76
+ ```yaml
77
+ shared_variables:
78
+ # String shorthand — same value in all environments
79
+ ADMIN_PORT: "8081"
80
+ PUBLIC_PORT: "8080"
81
+
82
+ # Object form — default value with per-environment overrides
83
+ JWT_SECRET:
84
+ value: ${JWT_SECRET_DEFAULT}
85
+ environments:
86
+ staging:
87
+ value: ${JWT_SECRET_STAGING}
88
+ production:
89
+ value: ${JWT_SECRET_PROD}
72
90
  ```
73
91
 
74
- ### Service configuration
92
+ Supports `${ENV_VAR}` syntax (resolved from your local environment or `--env-file`) and `${{shared.OTHER_VAR}}` self-references.
75
93
 
76
- Each service can be defined inline or via a template:
94
+ > **Note:** Shared variables cannot contain `${{service.VAR}}` cross-service references. Railway resolves shared variables without a service context.
95
+
96
+ ### Volumes
97
+
98
+ Volumes are declared at the top level with optional per-environment overrides. Services reference them by name.
77
99
 
78
100
  ```yaml
101
+ volumes:
102
+ pg-data:
103
+ size_mb: 50000
104
+ region: us-east4
105
+ environments:
106
+ production:
107
+ size_mb: 100000
108
+
109
+ redis-data: {} # Minimal declaration — Railway defaults
110
+
79
111
  services:
80
- # Inline service (Docker image)
81
- redis:
112
+ postgres:
82
113
  source:
83
- image: redis:7
84
- variables:
85
- ALLOW_EMPTY_PASSWORD: "yes"
114
+ image: postgres:17
115
+ volume: # Reference a declared volume
116
+ name: pg-data
117
+ mount: /var/lib/postgresql/data
118
+ ```
86
119
 
87
- # Inline service (GitHub repo)
88
- api:
89
- source:
90
- repo: myorg/my-api
91
- branch: main
120
+ Every volume referenced by a service must be declared in the `volumes` block.
121
+
122
+ ### Buckets
123
+
124
+ S3-compatible Railway buckets. The key is the bucket name.
125
+
126
+ ```yaml
127
+ buckets:
128
+ media-uploads:
129
+ region: iad
130
+ environments:
131
+ eu-production:
132
+ region: fra
133
+
134
+ logs: {} # Minimal — uses default region
135
+ ```
136
+
137
+ ### Services
138
+
139
+ Each service defines defaults that apply to all environments. Per-environment overrides go under `environments.<name>`:
92
140
 
93
- # Template-based service
141
+ ```yaml
142
+ services:
94
143
  web:
95
- template: ../services/web.yaml
96
- params:
97
- tag: v1.2.3
144
+ source:
145
+ repo: myorg/web-app
146
+ start_command: npm start
98
147
  variables:
99
- EXTRA_VAR: override-value
148
+ PORT: "3000"
149
+ environments:
150
+ staging:
151
+ source:
152
+ repo: myorg/web-app
153
+ branch: develop
154
+ production:
155
+ source:
156
+ repo: myorg/web-app
157
+ branch: main
158
+ wait_for_ci: true
159
+
160
+ # Service without environments block — exists in all environments
161
+ redis:
162
+ source:
163
+ image: redis:7
164
+
165
+ # Service scoped to specific environments
166
+ debug-tools:
167
+ source:
168
+ image: debug:latest
169
+ environments:
170
+ staging: {} # Only in staging
100
171
  ```
101
172
 
102
- ### Full service options
173
+ #### Service scope rules
174
+
175
+ - Service **has** `environments` block -> only exists in environments listed there
176
+ - Service **has no** `environments` block -> exists in ALL declared environments
177
+
178
+ #### Merge rules
179
+
180
+ When a service has per-environment overrides:
181
+
182
+ | Field type | Merge behavior |
183
+ |------------|---------------|
184
+ | `params`, `variables` | Shallow merge (override keys replace defaults) |
185
+ | `domains`, `source`, `volume`, `regions`, `healthcheck`, `build` | Replace entirely |
186
+ | Scalar fields (`start_command`, etc.) | Override replaces |
187
+
188
+ ---
103
189
 
104
- Every option below can be used on both inline services and service templates.
190
+ ### Service fields reference
191
+
192
+ Every field below can be used on service defaults, per-environment overrides, and templates.
105
193
 
106
194
  #### Source
107
195
 
196
+ Source is a discriminated union — use **either** `repo` or `image`, not both.
197
+
108
198
  ```yaml
199
+ # Repo source — deploy from a GitHub repository
109
200
  source:
110
- image: nginx:latest # Docker image (Docker Hub, GHCR, etc.)
111
- # OR
112
- repo: myorg/my-repo # GitHub repository
113
-
114
- branch: main # Branch to deploy from (GitHub repos)
115
- check_suites: true # Wait for GitHub Actions to pass before deploying
201
+ repo: myorg/my-repo
202
+ branch: main # Branch to deploy from
203
+ root_directory: /packages/api # Root directory (monorepo support)
204
+ wait_for_ci: true # Wait for GitHub Actions to pass before deploying
205
+ ```
116
206
 
117
- registry_credentials: # For private container registries
118
- username: ${REGISTRY_USER}
119
- password: ${REGISTRY_PASS}
207
+ ```yaml
208
+ # Image source — deploy from a container image
209
+ source:
210
+ image: nginx:latest # Docker image (Docker Hub, GHCR, etc.)
211
+ registry_credentials: # For private container registries
212
+ username: ${REGISTRY_USER}
213
+ password: ${REGISTRY_PASS}
214
+ auto_updates: # Auto-update schedule for image-based services
215
+ monday:
216
+ start_hour: 0
217
+ end_hour: 6
218
+ friday:
219
+ start_hour: 0
220
+ end_hour: 6
120
221
  ```
121
222
 
122
223
  #### Build
123
224
 
225
+ Build is a discriminated union — fields depend on the `builder` value.
226
+
124
227
  ```yaml
125
- builder: NIXPACKS # RAILPACK (default), NIXPACKS, HEROKU, PAKETO
126
- build_command: npm run build # Custom build command
127
- dockerfile_path: Dockerfile.prod # Path to Dockerfile (uses Railpack with Dockerfile)
128
- root_directory: /packages/api # Root directory (monorepo support)
129
- watch_patterns: # File patterns that trigger deploys
130
- - /packages/api/src/**
131
- - /packages/shared/**
132
- railway_config_file: railway.toml # Path to railway.json/toml for config-as-code
133
- metal: true # Enable Railway Metal builds (service-level, see note below)
228
+ # Railpack (default)
229
+ build:
230
+ builder: railpack
231
+ command: npm run build # Custom build command
232
+ watch_patterns: # File patterns that trigger deploys
233
+ - /packages/api/src/**
234
+ metal: true # Enable Metal build environment (faster builds)
134
235
  ```
135
236
 
136
- **Note:** Some settings are **service-level** in Railway (applied globally, not per-environment): `metal`, service creation, and service deletion. If you manage multiple environments for the same project, these settings will affect all environments regardless of which YAML file sets them.
237
+ ```yaml
238
+ # Nixpacks
239
+ build:
240
+ builder: nixpacks
241
+ command: npm run build
242
+ watch_patterns:
243
+ - /packages/api/src/**
244
+ metal: true
245
+ ```
246
+
247
+ ```yaml
248
+ # Dockerfile
249
+ build:
250
+ builder: dockerfile
251
+ dockerfile_path: Dockerfile.prod # Path to Dockerfile
252
+ watch_patterns:
253
+ - /packages/api/src/**
254
+ metal: true
255
+ ```
256
+
257
+ `railway_config_file` is a separate service-level field (not part of `build`):
258
+
259
+ ```yaml
260
+ railway_config_file: railway.toml # Path to railway.json/toml in the repository
261
+ ```
137
262
 
138
263
  #### Deploy
139
264
 
140
265
  ```yaml
141
- start_command: npm start # Custom start command
142
- pre_deploy_command: # Run before deployment (e.g., migrations)
266
+ start_command: npm start # Custom start command
267
+
268
+ pre_deploy_command: # Run before deployment (e.g., migrations)
143
269
  - npm run migrate
144
270
  - npm run seed
145
- cron_schedule: "*/5 * * * *" # Cron schedule (for scheduled jobs)
146
- healthcheck: # HTTP healthcheck
271
+
272
+ cron_schedule: "*/5 * * * *" # Cron schedule (5-field format)
273
+ # Note: cron forces restart_policy to NEVER
274
+ # and disables serverless
275
+
276
+ healthcheck: # HTTP healthcheck
147
277
  path: /health
148
- timeout: 300 # Timeout in seconds (default: 300)
149
- restart_policy: ON_FAILURE # ALWAYS, NEVER, or ON_FAILURE
150
- restart_policy_max_retries: 10 # Max retries (only with ON_FAILURE)
151
- sleep_application: true # Enable serverless sleeping
152
- draining_seconds: 30 # Graceful shutdown timeout (seconds between SIGTERM and SIGKILL)
153
- overlap_seconds: 10 # Blue-green deploy overlap duration
278
+ timeout: 300 # Timeout in seconds (default: 300)
279
+
280
+ # Restart policy string shorthand or object with max_retries
281
+ restart_policy: always # always, never, or on_failure
282
+
283
+ restart_policy: # Object form for on_failure with retries
284
+ type: on_failure
285
+ max_retries: 5
286
+
287
+ serverless: true # Enable serverless sleeping (scale to zero when idle)
288
+ draining_seconds: 30 # Graceful shutdown timeout (SIGTERM to SIGKILL)
289
+ overlap_seconds: 10 # Blue-green deploy overlap duration
154
290
  ```
155
291
 
156
292
  #### Networking
@@ -158,31 +294,35 @@ overlap_seconds: 10 # Blue-green deploy overlap duration
158
294
  ```yaml
159
295
  # Custom domains
160
296
  domains:
161
- - app.example.com # Simple domain
162
- - domain: api.example.com # Domain with target port
297
+ - app.example.com # Simple domain
298
+ - domain: api.example.com # Domain with target port
163
299
  target_port: 8080
164
300
 
165
- # Railway-provided domain
166
- railway_domain: true # Generate a .up.railway.app domain
167
- railway_domain: # ...with a specific target port
301
+ # Railway-provided domain (*.up.railway.app)
302
+ railway_domain:
168
303
  target_port: 3000
169
304
 
170
- # TCP proxies (for non-HTTP services like databases)
171
- tcp_proxies: [5432, 6379] # One or more ports
305
+ # TCP proxy (for non-HTTP services like databases)
306
+ tcp_proxy: 5432
307
+
308
+ # Private networking
309
+ private_hostname: postgres # Internal DNS hostname for service-to-service communication
172
310
 
173
311
  # Outbound networking
174
- ipv6_egress: true # Enable IPv6 outbound traffic
175
- static_outbound_ips: true # Assign permanent outbound IP addresses
312
+ ipv6_egress: true # Enable IPv6 outbound traffic
313
+ static_outbound_ips: true # Assign permanent outbound IP addresses
176
314
  ```
177
315
 
178
316
  #### Scaling
179
317
 
180
318
  ```yaml
181
- region: # Deployment region
182
- region: us-east-1
183
- num_replicas: 3 # Horizontal replicas (default: 1)
319
+ regions: us-east4 # Single region (1 replica)
320
+ # or
321
+ regions: # Multi-region with replica counts
322
+ us-east4: 3
323
+ us-west1: 1
184
324
 
185
- limits: # Resource limits per replica
325
+ limits: # Resource limits per replica
186
326
  memory_gb: 8
187
327
  vcpus: 4
188
328
  ```
@@ -190,9 +330,9 @@ limits: # Resource limits per replica
190
330
  #### Storage
191
331
 
192
332
  ```yaml
193
- volume: # Persistent volume
194
- mount: /data # Mount path (must be absolute)
195
- name: my-data
333
+ volume: # Reference a top-level volume
334
+ name: pg-data # Must match a key in the volumes block
335
+ mount: /var/lib/postgresql/data # Absolute mount path
196
336
  ```
197
337
 
198
338
  #### Variables
@@ -215,25 +355,17 @@ variables:
215
355
  | `%{service_name}` | At config load time | Built-in: the service's config key |
216
356
  | `null` | N/A | Marks a variable for deletion |
217
357
 
218
- **Important:** Shared variables (`shared_variables`) cannot contain `${{service.VAR}}` references Railway resolves shared variables without a service context, so cross-service references will resolve to empty strings. Use `${{service.VAR}}` references directly in service variables instead, and use shared variables only for plain values or `${{shared.OTHER_VAR}}` self-references.
219
-
220
- `%{param}` is expanded first, so it can be used inside `${{}}` Railway references. This is useful for templates that need to reference their own or other services' variables:
358
+ `%{param}` is expanded first, so it can be used inside `${{}}` Railway references:
221
359
 
222
360
  ```yaml
223
361
  variables:
224
- # Reference own service's variable (resolves %{service_name} at config time,
225
- # Railway resolves the ${{}} reference at runtime)
226
362
  DATABASE_URL: ${{%{service_name}.DATABASE_URL}}
227
-
228
- # Reference another service by param
229
363
  REDIS_URL: ${{%{cache_service}.REDIS_URL}}
230
364
  ```
231
365
 
232
366
  ### Service templates
233
367
 
234
- Templates extract reusable service definitions with parameterized values.
235
-
236
- The built-in `%{service_name}` param is always available and resolves to the service's key in the config (e.g., `web`, `api`). It cannot be overridden.
368
+ Templates extract reusable service definitions with parameterized values. The built-in `%{service_name}` param resolves to the service's key in the config.
237
369
 
238
370
  ```yaml
239
371
  # services/web.yaml
@@ -258,105 +390,156 @@ healthcheck:
258
390
  path: /health
259
391
  timeout: 300
260
392
 
261
- region:
262
- region: us-east-1
263
- num_replicas: 1
393
+ regions: us-east4
264
394
  ```
265
395
 
266
- Referenced from an environment config:
396
+ Referenced from a project config:
267
397
 
268
398
  ```yaml
269
399
  services:
270
400
  web:
271
- template: ../services/web.yaml
401
+ template: services/web.yaml
272
402
  params:
273
- tag: v2.0.0
274
- replicas: "3"
275
- variables:
276
- EXTRA: added-by-env # Merged with template variables
277
- APP_VERSION: null # Deletes the template-defined variable
278
- domains:
279
- - production.example.com # Overrides template domain
403
+ replicas: "1"
404
+ environments:
405
+ staging:
406
+ params:
407
+ tag: alpha
408
+ production:
409
+ params:
410
+ tag: v2.0.0
411
+ replicas: "3"
412
+ variables:
413
+ EXTRA: added-by-env
414
+ APP_VERSION: null # Deletes the template-defined variable
415
+ domains:
416
+ - production.example.com # Overrides template domains
280
417
  ```
281
418
 
282
- Template override precedence: environment config values override template values for `source`, `domains`, and `variables`.
283
-
284
419
  ### Complete example
285
420
 
286
421
  ```yaml
287
- # yaml-language-server: $schema=./schemas/environment.schema.json
422
+ # yaml-language-server: $schema=./schemas/project.schema.json
288
423
  project: My SaaS App
289
- environment: production
424
+ environments:
425
+ - staging
426
+ - production
290
427
 
291
428
  shared_variables:
292
- APP_ENV: production
293
- SENTRY_DSN: ${SENTRY_DSN}
429
+ APP_PORT: "3000"
430
+ SENTRY_DSN:
431
+ value: ${SENTRY_DSN_DEFAULT}
432
+ environments:
433
+ production:
434
+ value: ${SENTRY_DSN_PROD}
435
+
436
+ volumes:
437
+ pg-data:
438
+ size_mb: 50000
439
+ environments:
440
+ production:
441
+ size_mb: 200000
442
+ redis-data: {}
443
+
444
+ buckets:
445
+ uploads:
446
+ region: iad
294
447
 
295
448
  services:
296
449
  web:
297
450
  source:
298
451
  repo: myorg/web-app
299
- branch: main
300
- check_suites: true
301
- builder: NIXPACKS
302
- build_command: npm run build
452
+ root_directory: /packages/web
453
+ build:
454
+ builder: nixpacks
455
+ command: npm run build
456
+ metal: true
303
457
  start_command: npm start
304
- root_directory: /packages/web
305
458
  pre_deploy_command: npm run migrate
306
459
  healthcheck:
307
460
  path: /health
308
461
  timeout: 60
309
- restart_policy: ON_FAILURE
310
- restart_policy_max_retries: 5
311
- domains:
312
- - app.example.com
313
- - domain: api.example.com
314
- target_port: 8080
315
- railway_domain: true
316
- region:
317
- region: us-east-1
318
- num_replicas: 2
319
- limits:
320
- memory_gb: 4
321
- vcpus: 2
462
+ restart_policy:
463
+ type: on_failure
464
+ max_retries: 5
465
+ serverless: true
466
+ railway_domain:
467
+ target_port: 3000
322
468
  variables:
323
469
  PORT: "3000"
324
470
  DATABASE_URL: ${{Postgres.DATABASE_URL}}
471
+ environments:
472
+ staging:
473
+ source:
474
+ repo: myorg/web-app
475
+ branch: develop
476
+ domains:
477
+ - staging.example.com
478
+ production:
479
+ source:
480
+ repo: myorg/web-app
481
+ branch: main
482
+ wait_for_ci: true
483
+ domains:
484
+ - app.example.com
485
+ - domain: api.example.com
486
+ target_port: 8080
487
+ regions:
488
+ us-east4: 2
489
+ limits:
490
+ memory_gb: 4
491
+ vcpus: 2
325
492
 
326
493
  postgres:
327
494
  source:
328
- image: postgres:16
495
+ image: postgres:17
496
+ private_hostname: postgres
329
497
  volume:
330
- mount: /var/lib/postgresql/data
331
498
  name: pg-data
332
- tcp_proxies: [5432]
499
+ mount: /var/lib/postgresql/data
500
+ tcp_proxy: 5432
333
501
  variables:
334
502
  POSTGRES_DB: myapp
335
503
 
336
504
  redis:
337
505
  source:
338
506
  image: redis:7-alpine
507
+ private_hostname: redis
339
508
  volume:
340
- mount: /data
341
509
  name: redis-data
342
- tcp_proxies: [6379]
510
+ mount: /data
511
+ tcp_proxy: 6379
343
512
 
344
513
  worker:
345
- template: ../services/worker.yaml
514
+ template: services/worker.yaml
346
515
  params:
347
516
  queue: default
348
- sleep_application: false
517
+ serverless: false
349
518
 
350
- buckets:
351
- uploads:
352
- name: user-uploads
519
+ cron:
520
+ source:
521
+ repo: myorg/web-app
522
+ root_directory: /packages/cron
523
+ cron_schedule: "0 0 * * *"
524
+ start_command: node scripts/cleanup.js
353
525
  ```
354
526
 
527
+ ## Known limitations
528
+
529
+ - **Region management.** Setting `regions` deploys 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.
530
+ - **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.
531
+ - **Custom domains** may require DNS verification to take effect.
532
+ - **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.
533
+ - **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.
534
+ - **Volume size/region** can only be set or increased, not cleared or reduced. Railway does not support shrinking volumes.
535
+ - **Volume mount removal** is supported via the `volumeDelete` mutation and requires the `--allow-data-loss` flag, since it permanently deletes the volume and its data.
536
+ - **Bucket deletion** is not supported by Railway's API. Buckets that are removed from config will be left in place with a warning.
537
+
355
538
  ## JSON schemas
356
539
 
357
540
  Editor support (autocompletion, validation) is available via JSON schemas:
358
541
 
359
- - `schemas/environment.schema.json` -- environment config files
542
+ - `schemas/project.schema.json` -- project config files
360
543
  - `schemas/service-template.schema.json` -- service template files
361
544
 
362
545
  ## Development