On June 5, 2026, Railway's changelog #0293 announced two things at once: Sandboxes moving into Priority Boarding, and Infrastructure as Code arriving experimentally in CLI 5.2.0. The IaC half of that announcement deserves a closer look than it got.
Railway users can now describe an entire project — services, databases, buckets, custom domains, environment variables, replicas — in a typed .railway/railway.ts file, preview the diff with railway config plan, and apply it with railway config apply. TypeScript authoring is now generally available, with Python and Go in beta. The old per-service Config as Code (railway.json / railway.toml) is deprecated, with a hard cutoff of December 1, 2026.
This is real, useful, long-requested functionality — and it raises a sharper question than "is the DSL nice." What does "declarative" actually mean when the thing doing the declaring is your file, but the thing doing the reconciling is a closed control plane you cannot inspect?
That question is the whole post. Here is the short answer up front: Railway's IaC gives you declarative intent with push-button application, which is genuinely better than clicking through a dashboard. What it does not give you — and structurally cannot, on a closed PaaS — is declarative operations: continuous reconciliation, self-healing, and a definition of your infrastructure that means something without the vendor's API behind it. The table below makes the distinction concrete, and the rest of the post walks through what each side wins, what it costs, and why the difference matters most at exactly the moment a team is trying to leave.
The three layers of "declarative"
Every declarative system has three layers, and "infrastructure as code" can mean owning any subset of them. The layers are: desired state (the file in your repo), actual state (what is really running), and the reconciler (the loop that compares the two and closes the gap). The honest comparison between Railway's IaC and GitOps on a fleet you own is a comparison of who owns each layer:
| Layer | Railway IaC | Flux / Argo CD on an owned fleet |
|---|---|---|
| Desired state | .railway/railway.ts in your repo — inspectable, reviewable, versioned | K8s manifests / Helm / Kustomize in your repo — inspectable, reviewable, versioned |
| Actual state | Lives in Railway's control plane; readable only through Railway's API and CLI | Lives in etcd on your cluster; readable with kubectl, auditable down to the wire |
| Reconciler | railway config apply from your laptop or CI — a push you trigger, running against a closed API | An in-cluster controller running continuously — pull-based, open-source, forkable |
| Drift detection | Next plan (or CI --detailed-exit-code gate) notices; nothing watches between runs | Controller notices within seconds and can auto-correct |
| Self-healing | None — a dashboard edit or platform-side change sits until someone re-applies | Built in — the controller reverts drift toward the declared state |
| Meaning without the vendor | Zero — service(), postgres(), bucket() are Railway API calls in a trench coat | Full — the manifests describe portable Kubernetes objects plus Cluster API machines |
Both columns start from the same place: a file you can diff in a pull request. They diverge everywhere after that. Railway's docs put the key sentence plainly: "Railway doesn't read .railway/ during deploys." Your file is evaluated by the CLI at apply time, pushed through the API, and then the platform forgets about it until the next push. A GitOps controller, by contrast, never stops reading your repo — the desired state is continuously asserted, not periodically submitted. That is not a small implementation detail. It is the difference between configuration management and a control loop, and everything else in this post follows from it.
What Railway's IaC genuinely gets right
Before the critique, the credit — because Railway built this carefully, and a fair accounting matters for the decision at the end. The plan/apply flow will be familiar to anyone who has used Terraform: railway config pull imports an existing project's live configuration into a human-editable file, omitting platform defaults and internal IDs, and rendering variable values as preserve() so secrets stay on Railway rather than landing in source.
railway config plan prints a Plan: 1 to add, 0 to change, 0 to destroy diff with secrets redacted as «hidden» by default. railway config apply re-plans immediately before committing, so a concurrent dashboard edit rejects the apply instead of silently winning. CI gets a pinned-plan flow (plan --out then apply --plan) guarded by the environment's configEtag, plus a --detailed-exit-code flag that exits 2 on pending changes so a pipeline can gate on drift. The official railwayapp/config GitHub Action wires this into pull requests: plan comments on every PR that touches .railway/, apply on merge, and project tokens scoped to a single environment so a workflow can only ever touch what it is supposed to.
There are thoughtful safety rails, too. Destructive operations need an explicit --confirm-destructive alongside --yes in non-interactive runs, so a stray flag cannot delete resources on its own. Multi-repo setups get named partials with CLI-enforced ownership: partial api owns the database, partial web owns the frontend, and neither file can touch the other's resources. And the migration story from the old system is explicit rather than weaselly — plans hard-stop on any service still managed by railway.json, forcing one source of truth per service, with the December 1 cutoff published in the docs.
For a team whose alternative is dashboard clicking or tribal deploy knowledge, this is a large, unambiguous step forward. Typed config, reviewable diffs, CI gating, and import-from-live-state cover the 80% of "infrastructure as code" that most small teams actually use. The remaining 20% is what the next two sections are about — and it is the 20% that decides whether your infrastructure definition is an asset you own or a convenience you rent.
What "Railway doesn't read .railway/ during deploys" costs you
The costs all trace back to the missing control loop. First, drift is silent between applies. Anyone with dashboard access can change a variable, scale a service, or attach a domain, and nothing notices until the next plan — which might be the next PR touching .railway/, or never, if the change happens in a service whose file rarely moves. The --detailed-exit-code CI gate only helps if it runs on a schedule against every environment, and even then it reports drift rather than repairing it. A Flux or Argo CD controller would have reverted the dashboard edit within its sync window. Railway's model treats the dashboard as a second writer with no referee.
Second, there is no self-healing. If the platform side changes under you — a default shifts, a region migrates, a managed database gets moved — your file has no mechanism to reassert itself. You re-run plan, read the diff, and decide. That is fine when the change is benign and you are watching. It is less fine at 3 AM, which is when platform-side changes tend to matter.
Third, the semantics are rented, not owned. Every resource in your file — service(), postgres(), bucket(), project() — is defined by Railway's API, versioned on Railway's schedule, and meaningful only while Railway exists to interpret it. The December 1 forced migration off Config as Code is the live exhibit: a vendor can and will sunset a config format, and your "code" migrates on their deadline. Today's .railway/railway.ts is one product generation away from being tomorrow's legacy format, and the importer that makes migration painless is itself a Railway tool that only exists while Railway does.
None of this is a criticism of Railway's engineering — it is a property of the architecture. A closed control plane cannot offer open reconciliation, because reconciliation is the control plane. The question is never "is Railway's IaC well built" (it is). The question is what you own on the day the relationship ends.
The exit test: what do you own on the day you leave?
Run the thought experiment concretely. Your team decides to leave Railway — for cost, for compliance, for reliability, for any of the reasons teams actually migrate. You run railway config pull one last time and get a tidy, typed, complete description of your project. What do you have?
You have a Railway-shaped artifact. postgres("postgres") describes a Railway managed database, not a Postgres you can start elsewhere — connection semantics, backup behavior, storage, and extensions are all Railway's. service("web", { build, start }) describes a Railway build-and-serve pipeline, not a container you can run — the builder, the routing mesh, and the TLS termination it assumes are Railway's. Environment variable references like db.env.DATABASE_URL resolve through Railway's service graph.
There is no --target self-hosted flag because there cannot be: the file never described infrastructure in the first place. It described requests to Railway's API, in a typed wrapper. Reproducing your system elsewhere means re-interpreting every resource by hand — reading the Railway config as documentation of intent and rewriting it against whatever the new platform understands.
Contrast that with the GitOps column. A repo of Kubernetes manifests plus Cluster API machine definitions describes objects that any conformant cluster can serve: Deployments, Services, Ingresses, Postgres via an operator you chose, machines via a provider you can swap. Moving from Hetzner to another bare-metal provider, or from one region to another, is a change of provider credentials and machine specs — the workload layer does not move at all. The reconciler itself (Flux, Argo CD, Cluster API controllers) is open source running on your machines; it has no vendor to sunset it and no API terms to change under you. Portability here is not a migration project. It is a property of the artifact.
And the "day you leave" is not hypothetical for Railway teams in 2026. On May 19–20, Google Cloud incorrectly suspended Railway's production account, and the platform went dark for roughly eight hours — API, dashboard, control plane, and databases down from 22:20 UTC to 06:14 UTC, across some 10 million hosted services. The cruelest detail came from Railway's own postmortem: as cached routing information expired, workloads running on AWS and Railway Metal became unreachable too, even though those containers were still healthy. The data plane was fine; the control plane decided otherwise.
Add the December 2025 fleet-wide resource exhaustion from a cryptominer exploit and a March 2026 caching misfire that reportedly served authenticated user data to the wrong users, and you get the pattern that actually drives migrations: not one outage, but the dawning realization that your uptime depends on systems you cannot see, governed by contracts you did not sign — Railway's contract with its upstream provider decided your availability that night. IaC does not change any of that. It makes the configuration reproducible. It does not make the platform accountable to you.
Who should use which
This is not an argument that Railway's IaC is bad, or that every team needs GitOps on owned machines. It is an argument for matching the tool to the stakes, with eyes open about which layer each tool owns:
- Railway IaC is enough when you are a small team with a few services, your compliance story tolerates a vendor-owned control plane, and your exit plan is "we would rewrite this in a week if we had to." The plan/apply flow, PR comments, and drift gating are genuinely good, and they cost you nothing to run. Take the win.
- GitOps on an owned fleet wins when any of these are true: you need continuous drift correction rather than periodic drift reporting; a regulator or a customer contract asks what reconciles your production state and "the vendor's API" is not an acceptable answer; your infrastructure definition must outlive any single provider; or you have been through a control-plane outage — yours or someone else's — and decided that "the containers were healthy but unreachable" is a sentence you never want to explain again.
The sensitivity is mostly about reversibility. The smaller and younger the system, the cheaper the Railway-shaped artifact is to reinterpret later, and the more the convenience outweighs the lock-in. The larger it grows — more services, more environments, more partials, more preserve()d secrets whose actual values live only in Railway — the more expensive the exit test gets, and the more each passing month of "we'll deal with portability later" compounds. That compounding is silent right up until the night it is not.
If that last paragraph made you want the GitOps column without building the fleet machinery yourself, that is exactly the gap Bex.co fills: push a git repo, get a running HTTPS service on machines you own, with the config, the reconciler, and the cluster all inspectable and forkable. Your infrastructure definition stays yours because the whole loop runs where you can see it.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



