Files
2026-09-19 13:41:53 -04:00
..
2026-09-19 13:41:53 -04:00
2026-09-19 13:10:34 -04:00
2026-09-19 13:41:53 -04:00

Infrastructure

The site on Railway: one project, bible, with a production and a dev environment, and one service, site, that runs the repo's Dockerfile in Virginia (us-east4-eqdc4a). Railway holds the certificates; server.py does the routing there as it does locally. DNS stays on Cloudflare.

env Railway environment URL
prod production https://bible.aerlinn.net
dev dev https://bible-dev.aerlinn.net
railway/       project, environments, service, custom domains, and their
               CNAME + TXT records on Cloudflare
railway-cli/   Railway's CLI as a pinned image (theirs is x86-only)

Tools

Nothing to install but Docker. Terraform and Railway's CLI are services in compose.yaml (profile tools), reading tokens from .env. From the repo root:

docker compose run --rm terraform -chdir=infra/railway plan
docker compose run --rm railway status

Commit .terraform.lock.hcl; it pins the provider builds.

First-time setup

  1. Tokens. A Railway workspace token (account settings -> Tokens, with the workspace selected; a project token cannot make projects), the Cloudflare token, and the state bucket's keys (below). cp .env.example .env and fill them in.

  2. Project and domains.

    docker compose run --rm terraform -chdir=infra/railway init
    docker compose run --rm terraform -chdir=infra/railway apply
    
  3. Deploy, below. Railway issues each certificate once it sees the CNAME; the first HTTPS requests can fail for a minute or two until then.

Deploying

python3 utils/compile_bible.py    # only when source/ changes
./deploy.sh dev
./deploy.sh prod

deploy.sh copies what the Dockerfile needs into .build/ and uploads that with railway up; Railway builds it and switches over when the new container is running. Not the repo itself: the CLI skips gitignored paths, which includes bible/, and telling it not to would send .env and source/ too. The build logs stream to the terminal, and the dashboard has them as well.

Terraform never deploys, and the provider cannot set anything Railway calls config-as-code (healthchecks, start commands); Railway's defaults do.

State

In a bucket on Railway: project tfstate, bucket tfstate (S3 name tfstate-9p20m8qvmqf8gqk-z, endpoint https://t3.storageapi.dev, US East). Every project keeps its state there under its own key, <project>/terraform.tfstate, with Terraform's lock file next to it. The bucket's keys are TFSTATE_* in .env; compose passes them to Terraform as AWS_*, the names the S3 backend reads.

The bucket is made outside Terraform, once: the provider has no bucket resource, and a bucket cannot hold the state that creates it. It was made through the API, since the CLI refuses a workspace token for init/link: projectCreate, then bucketCreate, then environmentPatchCommit with {buckets: {<id>: {region: "iad", isCreated: true}}}.

A new project's stack copies the backend "s3" block from railway/main.tf with a new key.

If the state is ever lost, nothing on Railway goes away; terraform import each resource back (IDs from the dashboard URLs; the Cloudflare records as <zone_id>/<id>).

Cost

The state bucket is $0.015 per GB-month: nothing, at a few KB.

Hobby plan: $5 a month, which includes $5 of usage. The service idles at a few tens of MB and next to no CPU in each environment ($10 per GB-month of memory, $20 per vCPU-month), so both fit in the included usage. Egress is $0.05/GB, about $0.0001 per new reader (the whole library, ~2.2 MB).