platepilot

Playground environment on AWS (single EC2 host)

What exists

Resource Value / name
EC2 instance platepilot-playground, t3.medium, Amazon Linux 2023, 30 GB encrypted gp3, IMDSv2, no SSH key
Elastic IP 52.206.216.97
Security group platepilot-playground: inbound UDP 51820 only (WireGuard). No HTTP port is reachable from the internet (changed 2026-09-21 after a Shepherd EC2AuthenticationCheck finding)
IAM role / instance profile platepilot-playground-ec2: AmazonSSMManagedInstanceCore + read on the artifact bucket
S3 bucket platepilot-playground-703777560800-us-east-1 holds platepilot-src.tgz (the source snapshot)
Everything is tagged project=platepilot-playground

On the host (/opt/platepilot/app, the same layout as the repository): docker compose runs Postgres 16 and LocalStack 4.14 (LocalStack published on 0.0.0.0:4566 through docker-compose.override.yml), and two node:22-bookworm containers run tsx src/main.ts and tsx src/worker.ts on the host network, exactly like the desktop setup. All containers restart automatically. .env on the host mirrors the desktop .env plus PHOTO_UPLOAD_PUBLIC_ENDPOINT_URL=http://52.206.216.97:4566, so presigned photo uploads from a phone go straight to the instance.

Access is through a WireGuard VPN terminated on the instance (wg0, 10.8.0.1/24, UDP 51820, one peer per device with a pre-shared key). Inside the tunnel:

The public address 52.206.216.97 answers only WireGuard handshakes. Client configs live on the dev desktop under ~/.platepilot-secrets/ (never in the repo); add a device by generating a key pair and PSK on the instance (wg genkey, wg genpsk), appending a [Peer] with the next 10.8.0.N/32 to /etc/wireguard/wg0.conf, and systemctl restart wg-quick@wg0.

Cost: roughly $35 per month while running (t3.medium ≈ $30, 30 GB gp3 ≈ $2.40, Elastic IP ≈ $3.60). Stopping the instance keeps the disk and IP (≈ $6/month) and preserves data; terminating removes everything (see teardown).

Security posture

Using it from the phone

  1. Install the WireGuard app (Play Store), tap + → Scan from QR code, scan the device’s config QR, and switch the tunnel on. It only routes 10.8.0.0/24, so the rest of the phone’s traffic is untouched.
  2. Install platepilot-vpn-*.apk (built with scripts/mobile/build-android-apk.sh http://10.8.0.1:3001 vpn), or set Me → About and help → Practice switches (local builds) → Server address to http://10.8.0.1:3001 in any local-device build, then close and reopen the app. Works on Wi-Fi and mobile data as long as the tunnel is on.

Operating it

All commands from the repository root with credentials for account 703777560800 (ada credentials update --account 703777560800 --provider conduit --role IibsAdminAccess-DO-NOT-DELETE). Instance id and addresses are recorded in .cache/deploy/playground.env on the desktop.

Redeploy the current working tree:

git ls-files -co --exclude-standard -z | tar --null -T - -czf .cache/deploy/platepilot-src.tgz
aws s3 cp .cache/deploy/platepilot-src.tgz s3://platepilot-playground-703777560800-us-east-1/platepilot-src.tgz
# render and run scripts/playground/instance-setup.sh on the instance via SSM (replaces /opt/platepilot/app,
# reinstalls, applies migrations, restarts api/worker; Postgres and LocalStack volumes are kept)
sed -e 's|__BUCKET__|platepilot-playground-703777560800-us-east-1|' -e 's|__PUBLIC_IP__|52.206.216.97|' scripts/playground/instance-setup.sh > /tmp/setup.sh
aws ssm send-command --instance-ids <instance-id> --document-name AWS-RunShellScript \
  --parameters "$(python3 -c 'import json;print(json.dumps({"commands":[open("/tmp/setup.sh").read()],"executionTimeout":["3600"]}))')"

Logs and shell:

aws ssm start-session --target <instance-id>          # interactive shell (needs the session-manager plugin)
sudo docker logs --tail 100 platepilot-api             # on the host
sudo docker ps

Stop / start (keeps data and address):

aws ec2 stop-instances --instance-ids <instance-id>
aws ec2 start-instances --instance-ids <instance-id>   # containers come back by themselves

Remove everything:

scripts/playground/teardown.sh

Verified on creation

From the instance against its own public address: /health 200; LocalStack health 200 on 4566; onboarding POST /adult-profile/onboarding 201; photo upload authorization returned a presigned URL on 52.206.216.97:4566, the PUT returned 200, complete queued a job and the worker marked it succeeded within 8 s; POST /planning/weeks/<monday>/generate returned a seven-day plan. (The dev desktop’s egress blocks outbound port 3001, so it cannot be used to test the public address; use a phone or a laptop off the corporate network.)