Source of truth for bootstrapping the stack, deploying, debugging, backups, and restore. Infrastructure details live in infra/terraform/README.md.
- One EC2 instance runs the site container. Caddy on the same host terminates
HTTP/HTTPS behind an Elastic IP, with an optional external DNS
Arecord. - The image digest and all host configuration live in
user_data, so every deploy replaces the instance and the running host never drifts from the code. - On boot the new host restores
julia.db,.state/, and Caddy's certificate store from the latest S3 backup. The daily backup timer alone bounds a replacement's data loss to about a day; the pre-deploy backup indeploy.shshrinks that to the few minutes before the apply. - The runtime environment is fixed and non-secret
(
/etc/rustc-perf-site.host.envon the host). If the app ever needs a secret (for example a GitHub token to avoid rate limits), add it as an SSM SecureString and wire it into that env file and the instance IAM policy.
You need AWS credentials that can create IAM roles and a VPC; see the
credentials note in infra/terraform/README.md
if you authenticate with aws login.
-
Fill in
infra/terraform/terraform.tfvars:aws_region = "us-east-1" name_prefix = "rustc-perf" instance_type = "t3a.medium" root_volume_size_gb = 100 site_hostname = "rustserver.perf.julialang.org" -
Create the image repository and backup bucket first, so the image push and the database seed have somewhere to go:
cd infra/terraform terraform init terraform apply -target=aws_ecr_repository.site -target=aws_s3_bucket.backups -
Seed the initial database — an archive containing
julia.dband.state/(with both checkpoint files), in the layout a backup produces:aws s3 cp latest.tar.gz "s3://$(terraform output -raw backup_bucket_name)/runtime/latest.tar.gz" -
Build, push, and apply:
SKIP_BACKUP=1 ./deploy.sh # no instance exists yet, so nothing to back up -
Point DNS at the instance: create an
Arecord forsite_hostname→terraform output -raw site_public_ip. Caddy obtains the Let's Encrypt certificate automatically once the name resolves. Until then (or ifsite_hostnameis unset) the site serves plain HTTP on the Elastic IP. -
Run the post-bootstrap checklist at the bottom of this document.
cd infra/terraform
./deploy.shThe script starts a backup on the instance, builds and pushes the image while
the backup runs, waits for the backup to succeed, then runs terraform apply
with the new digest — which replaces the instance. Terraform prompts before
replacing anything. Pass terraform apply flags through
(./deploy.sh -auto-approve), or set SKIP_BACKUP=1 to skip the backup and
accept rolling back to the last daily one.
Host-configuration changes deploy the same way — edit the code and run
./deploy.sh. A bare terraform apply fails the image-digest precondition on
purpose: only deploy.sh supplies -var site_container_image=..., so every
replacement ships an image that was just built and pushed.
Expect a couple of minutes of downtime while the new instance boots, pulls the image, and restores the database from S3. Julia precompilation is baked into the image, so it does not add to this.
Log on with Session Manager and read the site unit's journal — that is the right first move for almost any problem:
INSTANCE_ID="$(cd infra/terraform && terraform output -raw instance_id)"
aws ssm start-session --target "$INSTANCE_ID"
sudo journalctl -u rustc-perf-site -n 200 --no-pager # or -f to followThe unit runs the container in the foreground, so the journal carries all app
output (restore, Julia orchestrator, site binary); sudo docker logs rustc-perf-site shows the same stream. For certificate issuance and proxy
errors, read the Caddy unit instead:
sudo journalctl -u rustc-perf-caddy -n 200 --no-pager.
Quick state checks:
sudo systemctl status rustc-perf-site rustc-perf-caddy
curl -I http://127.0.0.1:2346/ # the backend itself
curl -I http://127.0.0.1/ # through Caddy
curl -I http://127.0.0.1/db/ # Datasette through CaddyThings to know while reading:
- First start on a fresh host takes a minute or two — image pull plus the S3
restore — during which the unit shows
activeand Caddy returns502 Bad Gateway. The service is ready whencurl -I http://127.0.0.1:2346/returns a normal HTTP response — not merely when the unit is running. Julia precompilation is baked into the image; if the journal streamsPrecompilingoutput on start, the baked caches were rejected becauseJULIA_CPU_TARGETin the Dockerfile does not cover this host's CPU — fix that rather than waiting it out. - If the restore finds no database and no usable backup (empty bucket,
download failure),
rustc-perf-restore-if-emptyfails the unit on purpose and systemd keeps retrying instead of starting an empty site. Seed the backup bucket (or fix backups), thensudo systemctl restart rustc-perf-site. - If the instance is missing from Session Manager, apply the stack (the IAM
instance profile is what grants SSM), wait a minute or two, then check
aws ssm describe-instance-information. - Datasette serves the public read-only query interface at
/db/. Its localhost listener is intentionally not reachable directly; query it through Caddy. It reads the live WAL database, so a query can see either the state before or after an ingestion transaction.
There is no SSH ingress; Session Manager is the only operator path.
Terraform installs a daily rustc-perf-backup.timer. Each run takes an online
snapshot with SQLite's .backup API (no downtime; a 30s busy timeout rides
out the orchestrator's write transactions), stages it under
/var/lib/rustc-perf-backup-staging on the root volume, and uploads:
- a timestamped archive at
<prefix>/archive/<hostname>/<ts>.tar.gz, expired by S3 lifecycle after 30 days; <prefix>/latest.tar.gz— an independent full copy (an S3 server-side copy, not a reference), which never expires. It is the restore source for a replaced host and survives arbitrarily long backup outages.
Each archive contains julia.db, .state/, caddy-data/ (the TLS
certificate store, so a replaced host reuses its certificate instead of
re-requesting from Let's Encrypt), and a metadata.txt with timestamp,
hostname, and image reference.
Manual run and inspection:
sudo /usr/local/bin/rustc-perf-backup
sudo journalctl -u rustc-perf-backup.service -n 100 --no-pager
systemctl list-timers rustc-perf-backup.timer
aws s3 ls s3://<backup-bucket>/runtime/ --recursiveIf a backup fails, check that journal and root-volume free space.
Automatic: on every service start, rustc-perf-restore-if-empty seeds
julia.db, .state/, and the Caddy store from latest.tar.gz (falling back
to the newest timestamped archive) — but only when no local database is
present. An existing database is never overwritten, so this is safe on every
start.
Manual (restoring a specific, older archive over current data):
-
sudo systemctl stop rustc-perf-site -
Download and unpack the archive; replace the contents of
/var/lib/rustc-perfwith it. -
Fix ownership, permissions, and stale SQLite sidecars:
sudo chown -R 10001:10001 /var/lib/rustc-perf sudo chmod 750 /var/lib/rustc-perf /var/lib/rustc-perf/.state sudo chmod 640 /var/lib/rustc-perf/julia.db sudo rm -f /var/lib/rustc-perf/julia.db-wal /var/lib/rustc-perf/julia.db-shm
-
sudo systemctl start rustc-perf-site && sudo systemctl restart rustc-perf-caddy -
Verify with the checks in "When Something Goes Wrong".
Test a real restore from S3 once before treating the backup path as production-ready.
terraform output -raw site_urlopens in a browser — over HTTPS ifsite_hostnameis set and itsArecord resolves tosite_public_ip.- On the instance:
curl -I http://127.0.0.1:2346/succeeds,/var/lib/rustc-perf/julia.dbis non-empty, and/var/lib/rustc-perf/.state/contains both checkpoint files. - The backup bucket has a timestamped archive after the first timer run (or
run
sudo /usr/local/bin/rustc-perf-backupby hand). ./deploy.shreplaces the instance and the new host comes back with its data and certificate intact.