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-2.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
-
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 .envand fill them in. -
Project and domains.
docker compose run --rm terraform -chdir=infra/railway init docker compose run --rm terraform -chdir=infra/railway apply -
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).