Deploying PackyTrace

The app runs on one AWS EC2 box: Terraform builds the infrastructure, cloud-init prepares the host, Docker Compose runs the containers, Caddy serves it over HTTPS. This page is the runbook — what to type, in order. For why it is shaped this way, see ADR-014.

All commands assume the AWS profile admin and region eu-central-1. Run Terraform from deployment/aws/terraform.

Prerequisites (one-time)

  • AWS CLI configured with an admin profile that can reach the account.
  • terraform (>= 1.10) and the gh CLI installed.
  • deployment/aws/terraform/terraform.tfvars filled in from the example (domain + GHCR token). Never commit it.

First deploy

  1. Publish the images once:

bash gh workflow run deploy-images.yml

  1. Apply the infrastructure:

bash cd deployment/aws/terraform terraform init terraform apply

  1. Add the DNS A record it prints at your registrar:

bash terraform output dns_record

  1. Watch the first boot until containers are healthy:

bash $(terraform output -raw ssm_session_command) sudo tail -f /var/log/packytrace-bootstrap.log sudo docker compose -f /opt/packytrace/docker-compose.yml ps

  1. Open the app:

bash terraform output -raw app_url

Deploy a new version

This is the everyday path. Merging does not change the box — it only publishes images. The box updates only when you bump the tag and redeploy.

  1. Merge to main. Semantic-release builds and pushes images to GHCR as vX.Y.Z and latest. Note the new version (git describe --tags --abbrev=0).

  2. Point the box at the new version:

bash aws ssm put-parameter \ --name /packytrace/config/image_tag \ --type String --overwrite \ --value vX.Y.Z \ --profile admin --region eu-central-1

  1. Redeploy:

bash $(terraform output -raw redeploy_command)

  1. Verify before calling it done: open terraform output -raw app_url, confirm /health responds, and run one real barcode scan.

Roll back

Same mechanic, pointed at the previous good version: set image_tag back and redeploy.

aws ssm put-parameter \
  --name /packytrace/config/image_tag \
  --type String --overwrite \
  --value vX.Y.Z-previous \
  --profile admin --region eu-central-1

$(terraform output -raw redeploy_command)

Caveat: this rolls back code, not the database. Services apply migrations forward at startup with no automatic down step (ADR-012), so a rollback is only clean if the migration was backward-compatible. Keep migrations additive (nullable/defaulted columns, deprecate before delete) so a rollback never strands the schema.

Everyday operations

Task Command
Open shell $(terraform output -raw ssm_session_command)
Check containers sudo docker compose -f /opt/packytrace/docker-compose.yml ps
Stream logs sudo docker compose -f /opt/packytrace/docker-compose.yml logs -f
Re-render config sudo /opt/packytrace/render-config.sh
Stop spending, keep data Stop the EC2 instance
Tear everything down terraform destroy

If something breaks

  • Boot looks stuck: sudo tail -f /var/log/packytrace-bootstrap.log.
  • A service is unhealthy: ... compose ps to see which, then ... compose logs -f <service>.
  • Config/secret looks wrong: re-run sudo /opt/packytrace/render-config.sh, then ... compose up -d.
  • Data is safe across restarts: all state lives on the separate /data EBS volume, so it survives instance replacement. terraform destroy is the only thing that removes it.

Known limits

  • No high availability — one box, single point of failure.
  • No automated backups yet — until then, /data is the one irreplaceable thing; never run terraform destroy casually.
  • Keycloak still ships demo setup that must be reviewed before real users.