Config as Code → IaC migration: railway.toml (NIXPACKS) vs actual Dockerfile build, and safe cutover order
dilson123-tech
HOBBYOP

5 days ago

Hi Railway team,

We're preparing to migrate a production service from Config as Code (railway.toml) to the new Infrastructure as Code (.railway/railway.ts) ahead of the 2026-12-01 deadline. Before touching production, we'd like official guidance on a few points where our observations, the docs and the CLI disagree. So far nothing has been applied: we have only run railway config migrate (local files only) and railway config plan.

Environment

  • One project, production environment, one service (referred to here as my-service), single replica in a single region
  • Source: GitHub repo, auto-deploy from main
  • CLI: railway 5.71.0; IaC SDK: npm [email protected] (Node 22+)

The discrepancy

The repository root contains a railway.toml:

[build]
builder = "NIXPACKS"
buildCommand = "pip install -r backend/requirements.txt -c backend/constraints-prod.txt || pip install -r requirements.txt -c backend/constraints-prod.txt"

[deploy]
startCommand = "bash backend/start.sh"

But the build actually runs from the root Dockerfile:

  • The build log shows load build definition from Dockerfile and runs the Dockerfile's steps.
  • The effective serviceManifest of the current deployment shows builder: DOCKERFILE.
  • The deployment's meta.configFile is "/railway.toml", but the service instance's railwayConfigFile is null.
  • railway config migrate status reports "No services in production read Config as Code".
  • The service instance shows builder: RAILPACK via the API, while railway config plan treats the builder as null.

railway config migrate generated a railway.ts that copies NIXPACKS + buildCommand from the toml. We edited it to match what really runs in production:

  • builder: "DOCKERFILE", dockerfilePath: "/Dockerfile", start: "bash backend/start.sh"
  • healthcheck path with a 600s timeout
  • same region, replica count, domain and port
  • variables unmanaged
  • partial = "my-service"

railway config plan reports "0 to add, 2 to change, 0 to destroy". Both updates are marked safe: build.builder/dockerfilePath (null → DOCKERFILE, /Dockerfile) and deploy.startCommand (null → bash backend/start.sh).

Questions

  1. Precedence of railway.toml vs IaC. The Config File path is null, yet deployments record configFile: "/railway.toml", so the root toml seems to be auto-detected. After railway config apply writes builder/startCommand to the service, does an existing root railway.toml still take precedence over the IaC-managed values? Docs say "configuration defined in code will always override values from the dashboard". Does that still hold for IaC-written values? Also, why does the toml's builder = "NIXPACKS" not take effect today while the Dockerfile wins? Is that expected behavior we can rely on?

  2. Does railway config apply trigger a redeploy? If it does, which configuration would that deployment use while the root railway.toml still exists?

  3. Is railway config migrate cutover required when the Config File path is already null? The CLI says: "Cutover clears each service's Config File path and redeploys it." In our case there is nothing to clear. Would cutover still redeploy, with which configuration, and is it needed for IaC to "own" the service? Since there are no saved paths, what would railway config migrate undo restore?

  4. Safe order for a zero-downtime migration. Our proposed sequence is below. Is it correct, or would you recommend a different order?

    1. Skip cutover, because the path is already null.
    2. railway config plan --out plan.json, then review it.
    3. railway config apply --plan plan.json.
    4. Verify the service instance: DOCKERFILE, /Dockerfile, startCommand.
    5. Remove railway.toml from the repo via PR and merge it, which triggers a normal deploy from main.
    6. Verify the new deployment: no configFile, Dockerfile build, healthcheck passing.

    Is there any step here that could deploy the service with no builder or start command, or with NIXPACKS?

  5. Docs vs CLI 5.71.0. The IaC docs say migrate --apply "clears the Railway Config File settings" and describe manual steps: remove railway.toml, clear the custom path, then plan/apply. They don't mention cutover or undo. The CLI 5.71.0 help says migrate --apply "only touches the filesystem", and Config as Code keeps applying "until you run railway config migrate cutover". After migrate, it suggested the order cutover → plan → apply. Which is authoritative for CLI 5.71.0? Is the plan's null vs the API's RAILPACK for the builder expected?

This is a business-critical production service, so we want to avoid any unplanned redeploy or build-system change. Thanks for your help!

$10 Bounty

3 Replies

Status changed to Awaiting Railway Response Railway • 5 days ago


Railway
BOT

5 days ago

This thread has been opened as a bounty so the community can help solve it.

Status changed to Open Railway • 5 days ago


halfabillion
HOBBY

4 days ago

I checked these against the CLI v5.71.0 source (github.com/railwayapp/cli, src/commands/config/migrate.rs and src/iac/change_set.rs) and the IaC docs. Where something is platform behaviour I couldn't verify, I say so.

1. Will the root railway.toml still win after apply?

Yes, while the file is in the commit being deployed. An empty Config File path doesn't mean "no Config as Code". When the path is empty, the builder auto-detects railway.toml (then railway.json) in the service root dir and then the repo root. That's why your deployments record configFile: "/railway.toml", and migrate.rs (default_cac_file) mirrors that lookup. Config as Code overrides service settings, and IaC writes service settings, so the toml keeps overriding whatever apply writes until the file is gone from the deployed commit. (After 2026‑12‑01 the files stop being read at all.)

Why builder = "NIXPACKS" loses to your Dockerfile today: I can't confirm that from the docs, so I wouldn't rely on it. Your deployment manifest (builder: DOCKERFILE) is the source of truth for now. Setting builder: "DOCKERFILE" explicitly in IaC and deleting the toml removes the ambiguity.

2. Does apply redeploy?

Yes. In the plan, severity: safe only means "not destructive". The separate field is deployEffect, and for build and deploy updates it is "deploy" (only config, groupId, cdn and some tracing changes are "none"). Check with:

railway config plan --json | jq '.. | objects | select(has("deployEffect")) | {path, deployEffect}'

That redeploy builds the service's current commit on main, so with railway.toml still there, the toml overrides again.

3. Is cutover needed when the path is already null?

No. It does nothing for you. cutover only targets service instances whose railwayConfigFile is a non-empty path ending in railway.toml/railway.json. With null it prints "Nothing to do. No declared services are still on Config as Code", writes no snapshot, and doesn't redeploy. undo would fail with "No cutover snapshot found (.railway/.cac-migration.json)". That's also why migrate status says "No services … read Config as Code": it only checks the path field, not the auto-detected root file. For you, deleting railway.toml is the cutover.

4. Order

Your order works, but steps 3 and 5 each cause a deploy, and between them the step‑3 deploy still reads the toml. Fewer moving parts:

  1. Put .railway/railway.ts and the deletion of railway.toml in the same PR. Don't merge yet.
  2. From that branch: railway config plan --out plan.json, and check that only build and startCommand change.
  3. railway config apply --plan plan.json. This redeploys main, which still has the toml, so it builds the same way as today (Dockerfile per your manifest). It's the same as your current deploy, just triggered once more.
  4. Merge the PR. The auto-deploy from main now has no CaC file, so it uses the service settings IaC just wrote (DOCKERFILE, /Dockerfile, bash backend/start.sh).
  5. Check the new deployment's details: no config-file icon / configFile, the Dockerfile build, and a passing healthcheck.

If you'd rather avoid the extra deploy in step 3, merge first and apply straight after. For that window (one deploy), the service runs with the dashboard values it already has. Since builder is null/RAILPACK and startCommand is null there, I wouldn't do it that way round for a production service.

One gap in your setup: if the healthcheck path and timeout also come from the toml today, keep them in railway.ts (you have). Otherwise they'd drop when the toml goes.

5. Docs vs CLI 5.71.0

For what the CLI actually does, go by the CLI. In 5.71.0, migrate --apply only writes files, and clearing Config File paths moved into migrate cutover/undo. The docs' "clears the Railway Config File settings" describes the older behaviour. The null vs RAILPACK difference is expected: the plan's diff normaliser treats builder: RAILPACK (and dockerfilePath: "Dockerfile", empty buildCommand) as the default and strips it, so it shows as null.

I used AI to help research and draft this, then checked each claim against the CLI source and docs linked above.


bpkroll
HOBBY

3 days ago

Thanks for the detailed investigation. I'm preparing a similar migration on Railway Hobby and have the same blank Config File path issue, but with a root-level railway.json.

Our production service runs a single Python worker with a persistent /data volume. The effective restart policy from railway.json is ON_FAILURE with 3 retries, while the dashboard displays 10 retries.

We are using CLI 5.71.0 and preparing .railway/railway.ts, but have not applied any configuration or performed a cutover.

Could Railway staff confirm three things?

  1. Default-file handoff: When the service instance's railwayConfigFile is null but deployments automatically read the root railway.json, is migrate cutover unnecessary? What supported procedure ensures that the existing file stops overriding IaC without an intermediate deployment using dashboard defaults?

  2. Deployment ordering: Is there a supported way to preview and stage the IaC configuration before removing railway.json, without triggering an additional deployment? Does config apply necessarily initiate a deployment for build, start-command or restart-policy changes?

  3. Persistent-volume safety: How can we confirm that the new IaC definition adopts the existing volume and retains its attachment, region, size and data rather than attempting to recreate or replace it? What rollback procedure is supported if the configuration handoff succeeds but the resulting deployment fails?

Our priority is preserving the existing worker, credentials, effective restart policy and database volume through a controlled maintenance migration.

We would appreciate Railway's recommended sequence for CLI 5.71.0 before performing any production-changing operation.


cristid
HOBBY

an hour ago

We have a similar case with 3 services in one project, but our Config File paths are NOT empty and use custom names (/railway.web.json, /railway.api.json, /railway.cron.json), builder NIXPACKS only from those files.

Could Railway staff confirm: (1) config apply while the custom path is still set: safe, and does it redeploy using the file values? (2) migrate cutover seems to target only paths ending in railway.toml/railway.json. For custom-named files, is clearing the path manually in Settings the supported step? (3) After clearing it and deploying, do the values written by IaC take effect?


Welcome!

Sign in to your Railway account to join the conversation.

Loading...