Repository navigation
feat(deploy): one-config Railway setup with Go IaC (deploy/platform.yaml) - #39
Merged
Merged
Conversation
jayleaton
marked this pull request as ready for review
October 22, 2026 11:22
…aml) deploy/platform.yaml decides the Railway project (admin, environments production | staging,production | staging,qa,production, previews, region, sizes); make railway-plan / railway-apply converge Railway to it. - deploy/ is its own Go module (railway-go-sdk + yaml stay out of the app go.mod); deploy/platform builds the IaC graph per environment (web + postgres, plus admin + admin-postgres), tested - deploy/.railway/railway.go is the shim the Railway CLI evaluates - cmd/railway-setup does what IaC cannot: project, empty environments, generated domains, sealed bootstrap secrets (AUTH_SECRET, ADMIN_AUTH_SECRET, DOPPLER_TOKEN placeholder, SECRETS_PROVIDER), PR-deploy toggle, railway up (upload mode), /healthz verify; it also repairs databases Railway gives an unevaluated secret() password - replaces the legacy root railway.json (Config as Code ends 2026-12-01) - Dockerfile drops RUN --mount=type=cache (Railway's builder rejects it) - CI: a deploy job vets, lints and tests deploy/; make ci runs deploy-check - docs: README "Deploy to Railway" (profiles, cost, Doppler, previews) and docs/railway-go-iac-issues.md (ready-to-file Railway issue drafts) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jayleaton
force-pushed
the
feat/railway-setup
branch
from
October 22, 2026 11:40
2bfc8ae to
3c002c1
Compare
It answers 503 when Doppler secrets go stale; a restart probe would turn a Doppler outage into a restart loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cident runbook - apps/admin/Dockerfile drops RUN --mount=type=cache (Railway's builder rejects it); apps/admin/railway.json removed (IaC owns the service) - generated AUTH_SECRET / ADMIN_AUTH_SECRET are no longer sealed: a rotation needs the old value in *_PREVIOUS / *_REVOKED, and a sealed value can never be read back (the next boot could not unwrap the 2FA data keys) - *_PREVIOUS / *_REVOKED are ignored by IaC so a rotation in progress is never planned for deletion; services on Doppler get no Railway copies - make railway-rotate-db: new superuser password per Postgres over railway ssh, then redeploy services that read DATABASE_URL from Railway - README: keep app secrets in Doppler, only DOPPLER_TOKEN in Railway (sealed), and how to rotate everything after a provider incident Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
One file decides the Railway project.
make railway-applymakes Railway match it using Railway Infrastructure as Code with Go authoring (beta). This replaces the legacyrailway.json, which Railway stops reading on 2026-12-01.Per environment, the graph contains:
web+postgres; withadmin: true, alsoadmin+ its ownadmin-postgres;/server migrate(or/admin migrate) as the pre-deploy command, and/healthzgating deploys;DATABASE_URL/ADMIN_DATABASE_URLas reference variables;Make targets:
railway-plan,railway-apply(CONFIRM_DESTRUCTIVE=1to allow deletes),railway-secrets(Doppler tokens, sealed),railway-up(upload mode,repo: ""),railway-verify.make cinow runsdeploy-check, and the CI Go job vets, lints and testsdeploy/.apps/admin/railway.jsonis removed: IaC carries its Dockerfile path, watch paths, pre-deploy and healthcheck.apps/admin/DockerfiledropsRUN --mount=type=cache, the same fix as the root Dockerfile (Railway's builder rejected it).Secrets layout (README "Where secrets live, and rotating after an incident"):
DOPPLER_TOKENlives in Railway, sealed.AUTH_SECRET/ADMIN_AUTH_SECRETare deliberately not sealed: a rotation needs the old value in*_PREVIOUS/*_REVOKED. Without it the next boot cannot unwrap the 2FA data keys (packages/auth/keyring.go), and a sealed value can never be read back.*_PREVIOUS/*_REVOKEDare IaC-ignored. Services already on Doppler get no Railway copies.make railway-rotate-dbrotates every Postgres superuser password overrailway sshand redeploys the services that readDATABASE_URLfrom Railway. It reuses the password-repair path that was verified live; the rotate command itself was not run against Railway (no test project during the incident). The branch is rebased (squashed) onto mainfcc7578, which includes admin (feat(admin): disable/remove admins, TOTP QR code, audited sign-ins and failed 2FA #37, feat(admin): admin mutations fail closed with an audit intent log, outcome rows and a reconciler #50) and live secrets (feat(secrets): DATABASE_URL and AUTH_SECRET rotate live; only DOPPLER_TOKEN needs a redeploy #40). CI gets a separatedeployjob, whichci-okrequires; it runs only whendeploy/,.golangci.ymlor the workflows change, and the go job ignoresdeploy/**.Secrets use packages/secrets' exact names:
SECRETS_PROVIDER(envuntil a real token exists, thendoppler) andDOPPLER_TOKEN(sealed, one per service and environment;DOPPLER_CONFIG/DOPPLER_API_URLare optional and not set)./healthzis only Railway's deploy gate, never a liveness probe: it answers 503 when Doppler secrets go stale, and restarts happen only on process exit. This is documented in graph.go and the README.Evidence
Real test in JJ's Railway account, project
ai-patterns-go-iac-test-20261022(workspace "JJ Eaton's Projects", Pro), CLI 5.71.0, SDK v0.6.0:environments: productionweb+postgrescreated; AUTH_SECRET/RESEND_API_KEY sealed, DOPPLER_TOKEN placeholder, SECRETS_PROVIDER=env; re-plan:✓ Your Railway configuration is already up to date.staging,productionno changes)staging,qa,production+admin: trueadmin+admin-postgresadded to all 3; 6 databases running; re-plan up to date in all 3admin: falsewithoutCONFIRM_DESTRUCTIVE=1Destructive Railway configuration changes require explicit confirmation), all 4 services still therepreviews: true{"prDeploys":true,"baseEnvironment":{"name":"staging"}}, re-plan up to dateDOPPLER_TOKEN_WEB_QA=… make railway-secretsDeploys (upload mode,
make railway-up). The Railway GitHub app has no access to this private repo, see below.make railway-verify:The pre-deploy migration ran (production deploy log):
[INFO] migrations appliedat 10:21:12, thenlistening addr=0.0.0.0:3000 public_url=https://web-production-5fb90.up.railway.app env=production. qa and staging logged the same.Idle footprint (Railway metrics) and estimated cost at $10/GB-month memory, $20/vCPU-month, $0.15/GB-month volume:
The whole test, roughly 2.5 h of 2–12 services, cost $0.0134. The project (
47eaa79e-bccb-4be2-b08f-30ab7c881022) was deleted afterwards; Railway schedules project deletion.Railway bugs found in the live test (handled here, drafted in
docs/railway-go-iac-issues.md):secret(32, "abc…")as its Postgres password. That is a public string, and the database is reachable through its public TCP proxy.railway-applydetects it and rotates the password overrailway ssh; qa's migration then succeeded with the new password.restartPolicyType: ON_FAILUREandwatchPatterns: []drift forever.railway updeploysSKIPPED.RAILWAY_ENV=stagingsilently switches the CLI to Railway's own staging API.RUN --mount=type=cachewithout a service-specific id. The Dockerfile on main could not build on Railway at all; this PR drops the cache mounts.Checks on the rebased branch:
make cipasses in 2m21s. Every coverage floor is met, including secrets 98.3%, admin services/api 100% and adminrepo 81.9%.deploy-checkreports 0 issues and thedeploy/platformtests pass. actionlint and gofmt are clean.Not verified:
repo: jayleaton/ai-patterns-gofailed with "You don't have access to the GitHub repo … install the Railway GitHub App". It needs the app installed on the repo.server migrate→migrations applied,admin migrate→admin migrations applied,/healthz200 on both, admin idle ≈ 70 MiB. Its Dockerfile now has the cache-mount fix.make railway-rotate-dband the README incident steps (not run against Railway).Merge Danger
Door: two-way. Nothing applies to Railway on merge;
make railway-applyis run by a human. Reverting restoresrailway.json. Projects already deployed with the oldrailway.jsonkeep their dashboard settings.Blast Radius: deployment. It affects anyone deploying the template to Railway. The app module and the binary are unchanged. The Dockerfile only drops build-cache mounts, so local
make dockerrebuilds compile from scratch and the image contents are the same.deploy/is in.dockerignore.🤖 Generated with Claude Code