← Corpus / self-host-stack / reminder
railway config pull does not pin database images — read the plan
A pulled Railway IaC config re-declares every postgres() and redis() at the helper's default version. Applying it unchanged proposes a major-version upgrade on live databases.
- Path
- reminders/Railway-IaC-Pull-Does-Not-Pin-Database-Images.md
- Authors
- Michael Staton
- Augmented with
- Claude Code on Claude Opus 5
- Tags
- Reminder · Railway · Infrastructure-As-Code · Postgres · Near-Miss
railway config pull does not pin database images
The near-miss (2026-08-09)
Adding one backup service to reach-edu via Railway IaC. Ran
railway config pull, added a pg-dump-outline block, ran railway config plan:
Plan: 1 to add, 7 to change, 0 to destroy
+ Create service pg-dump-outline
~ Update twenty-postgres source.image (unset → "postgres:18")
~ Update outline-postgres source.image (unset → "postgres:18")
~ Update postiz-postgres source.image (unset → "postgres:18")
~ Update temporal-postgres source.image (unset → "postgres:18")
~ Update twenty-redis source.image (unset → "redis:8")
~ Update outline-redis source.image (unset → "redis:8")
~ Update postiz-redis source.image (unset → "redis:8")
Those databases were on 16 / 17-alpine and Redis 7 / 7.2, all with live volumes. A Postgres major-version bump is not an upgrade — the on-disk data directory format is incompatible, so the new server refuses to start on the old volume. Applying would have taken down every database in the project, including a CRM with real client data.
Nothing in the pull warned about it. The postgres() and redis() IaC helpers
emit no image, and Railway resolves “no image” to latest at apply time.
The rule
Never railway config apply without reading the plan line by line. The
service you intended to add will be one line among several you did not intend.
Before applying, pin every database to what is actually running:
railway service list --json | python3 -c "…" # read the real images first
twentyPostgres.source = image("postgres:16");
temporalPostgres.source = image("postgres:16");
outlinePostgres.source = image("postgres:16");
postizPostgres.source = image("postgres:17-alpine");
twentyRedis.source = image("redis:7");
outlineRedis.source = image("redis:7");
postizRedis.source = image("redis:7.2");
Then re-plan. The only acceptable plan is one where every line is a change you
asked for — in that case 1 to add, 0 to change, 0 to destroy.
Second trap in the same tool
IaC deletes variables the file does not declare. Anything set with
railway variable set after a pull is absent from the file and is removed on the
next apply. Declare secrets as preserve() so they survive:
env: {
AWS_ACCESS_KEY_ID: preserve(),
AWS_SECRET_ACCESS_KEY: preserve(),
BACKUP_DATABASE_URL: preserve(),
}
The plan does show these as - Delete variable …, which is another reason to
read it rather than skim it.
Why we use IaC at all
railway add -r <github repo> returns Unauthorized even with a valid session —
creating a GitHub-sourced service needs the Railway GitHub App scope the CLI
token doesn’t carry. IaC is the only working path for those, so this trap is not
avoidable by preferring the CLI.
Setup note: railway config requires the railway npm package (not
@railway/sdk, not railway-ts-sdk) installed in the directory containing
.railway/railway.ts.
Related
client-stacks/reach-edu/outline/stack.md— where this was hit (gitignored)docs/outline/setup.md— the Outline runbook- [[Normalize-Paths-Everywhere]]