@tachyon-gg/railway-deploy 0.2.9 → 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 +323 -137
  2. package/dist/index.js +4330 -2127
  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,108 +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
+ ---
189
+
190
+ ### Service fields reference
103
191
 
104
- Every option below can be used on both inline services and service templates.
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
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)
235
+ ```
236
+
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
133
261
  ```
134
262
 
135
263
  #### Deploy
136
264
 
137
265
  ```yaml
138
- start_command: npm start # Custom start command
139
- 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)
140
269
  - npm run migrate
141
270
  - npm run seed
142
- cron_schedule: "*/5 * * * *" # Cron schedule (for scheduled jobs)
143
- 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
144
277
  path: /health
145
- timeout: 300 # Timeout in seconds (default: 300)
146
- restart_policy: ON_FAILURE # ALWAYS, NEVER, or ON_FAILURE
147
- restart_policy_max_retries: 10 # Max retries (only with ON_FAILURE)
148
- sleep_application: true # Enable serverless sleeping
149
- draining_seconds: 30 # Graceful shutdown timeout (seconds between SIGTERM and SIGKILL)
150
- 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
151
290
  ```
152
291
 
153
292
  #### Networking
@@ -155,31 +294,35 @@ overlap_seconds: 10 # Blue-green deploy overlap duration
155
294
  ```yaml
156
295
  # Custom domains
157
296
  domains:
158
- - app.example.com # Simple domain
159
- - domain: api.example.com # Domain with target port
297
+ - app.example.com # Simple domain
298
+ - domain: api.example.com # Domain with target port
160
299
  target_port: 8080
161
300
 
162
- # Railway-provided domain
163
- railway_domain: true # Generate a .up.railway.app domain
164
- railway_domain: # ...with a specific target port
301
+ # Railway-provided domain (*.up.railway.app)
302
+ railway_domain:
165
303
  target_port: 3000
166
304
 
167
- # TCP proxies (for non-HTTP services like databases)
168
- 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
169
310
 
170
311
  # Outbound networking
171
- ipv6_egress: true # Enable IPv6 outbound traffic
172
- 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
173
314
  ```
174
315
 
175
316
  #### Scaling
176
317
 
177
318
  ```yaml
178
- region: # Deployment region
179
- region: us-east-1
180
- 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
181
324
 
182
- limits: # Resource limits per replica
325
+ limits: # Resource limits per replica
183
326
  memory_gb: 8
184
327
  vcpus: 4
185
328
  ```
@@ -187,9 +330,9 @@ limits: # Resource limits per replica
187
330
  #### Storage
188
331
 
189
332
  ```yaml
190
- volume: # Persistent volume
191
- mount: /data # Mount path (must be absolute)
192
- 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
193
336
  ```
194
337
 
195
338
  #### Variables
@@ -212,25 +355,17 @@ variables:
212
355
  | `%{service_name}` | At config load time | Built-in: the service's config key |
213
356
  | `null` | N/A | Marks a variable for deletion |
214
357
 
215
- **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.
216
-
217
- `%{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:
218
359
 
219
360
  ```yaml
220
361
  variables:
221
- # Reference own service's variable (resolves %{service_name} at config time,
222
- # Railway resolves the ${{}} reference at runtime)
223
362
  DATABASE_URL: ${{%{service_name}.DATABASE_URL}}
224
-
225
- # Reference another service by param
226
363
  REDIS_URL: ${{%{cache_service}.REDIS_URL}}
227
364
  ```
228
365
 
229
366
  ### Service templates
230
367
 
231
- Templates extract reusable service definitions with parameterized values.
232
-
233
- 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.
234
369
 
235
370
  ```yaml
236
371
  # services/web.yaml
@@ -255,105 +390,156 @@ healthcheck:
255
390
  path: /health
256
391
  timeout: 300
257
392
 
258
- region:
259
- region: us-east-1
260
- num_replicas: 1
393
+ regions: us-east4
261
394
  ```
262
395
 
263
- Referenced from an environment config:
396
+ Referenced from a project config:
264
397
 
265
398
  ```yaml
266
399
  services:
267
400
  web:
268
- template: ../services/web.yaml
401
+ template: services/web.yaml
269
402
  params:
270
- tag: v2.0.0
271
- replicas: "3"
272
- variables:
273
- EXTRA: added-by-env # Merged with template variables
274
- APP_VERSION: null # Deletes the template-defined variable
275
- domains:
276
- - 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
277
417
  ```
278
418
 
279
- Template override precedence: environment config values override template values for `source`, `domains`, and `variables`.
280
-
281
419
  ### Complete example
282
420
 
283
421
  ```yaml
284
- # yaml-language-server: $schema=./schemas/environment.schema.json
422
+ # yaml-language-server: $schema=./schemas/project.schema.json
285
423
  project: My SaaS App
286
- environment: production
424
+ environments:
425
+ - staging
426
+ - production
287
427
 
288
428
  shared_variables:
289
- APP_ENV: production
290
- 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
291
447
 
292
448
  services:
293
449
  web:
294
450
  source:
295
451
  repo: myorg/web-app
296
- branch: main
297
- check_suites: true
298
- builder: NIXPACKS
299
- build_command: npm run build
452
+ root_directory: /packages/web
453
+ build:
454
+ builder: nixpacks
455
+ command: npm run build
456
+ metal: true
300
457
  start_command: npm start
301
- root_directory: /packages/web
302
458
  pre_deploy_command: npm run migrate
303
459
  healthcheck:
304
460
  path: /health
305
461
  timeout: 60
306
- restart_policy: ON_FAILURE
307
- restart_policy_max_retries: 5
308
- domains:
309
- - app.example.com
310
- - domain: api.example.com
311
- target_port: 8080
312
- railway_domain: true
313
- region:
314
- region: us-east-1
315
- num_replicas: 2
316
- limits:
317
- memory_gb: 4
318
- vcpus: 2
462
+ restart_policy:
463
+ type: on_failure
464
+ max_retries: 5
465
+ serverless: true
466
+ railway_domain:
467
+ target_port: 3000
319
468
  variables:
320
469
  PORT: "3000"
321
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
322
492
 
323
493
  postgres:
324
494
  source:
325
- image: postgres:16
495
+ image: postgres:17
496
+ private_hostname: postgres
326
497
  volume:
327
- mount: /var/lib/postgresql/data
328
498
  name: pg-data
329
- tcp_proxies: [5432]
499
+ mount: /var/lib/postgresql/data
500
+ tcp_proxy: 5432
330
501
  variables:
331
502
  POSTGRES_DB: myapp
332
503
 
333
504
  redis:
334
505
  source:
335
506
  image: redis:7-alpine
507
+ private_hostname: redis
336
508
  volume:
337
- mount: /data
338
509
  name: redis-data
339
- tcp_proxies: [6379]
510
+ mount: /data
511
+ tcp_proxy: 6379
340
512
 
341
513
  worker:
342
- template: ../services/worker.yaml
514
+ template: services/worker.yaml
343
515
  params:
344
516
  queue: default
345
- sleep_application: false
517
+ serverless: false
346
518
 
347
- buckets:
348
- uploads:
349
- 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
350
525
  ```
351
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
+
352
538
  ## JSON schemas
353
539
 
354
540
  Editor support (autocompletion, validation) is available via JSON schemas:
355
541
 
356
- - `schemas/environment.schema.json` -- environment config files
542
+ - `schemas/project.schema.json` -- project config files
357
543
  - `schemas/service-template.schema.json` -- service template files
358
544
 
359
545
  ## Development