Deployment
Pipeline
Push to main
│
▼
GitHub Actions CI
├── make check (fmt-check, clippy, test, sqlx-check)
└── Build Docker images
├── mailman-api (--target api)
└── mailman-worker (--target worker)
│
▼
Push to ECR (via GitHub OIDC → IAM role)
│
▼
ECS force-new-deployment (pulls latest image)
│
▼
Rolling update (new tasks start, old tasks drain)How It Works
CI runs on every push to main. Tests must pass, clippy must be clean, fmt must be correct.
Docker images are built using the multi-stage Dockerfile with
cargo-cheffor dependency caching. Two targets:apiandworker.Images are pushed to ECR using GitHub OIDC authentication (no static AWS credentials stored in GitHub).
ECS services update via
force-new-deployment. ECS pulls the new image, starts new tasks, and drains old ones once the new tasks are healthy.Health checks gate the rollout. The ALB health check (
GET /health) must pass before the new task receives traffic. If it fails, ECS rolls back automatically.
Database Migrations
Migrations run automatically on service startup via SQLx. The API binary runs pending migrations before accepting traffic.
Safe Migration Checklist
Before writing a migration, check:
| Safe? | Operation | Why |
|---|---|---|
| ✅ | Add a new table | No existing queries affected |
| ✅ | Add a nullable column | Existing rows get NULL |
| ✅ | Add an index | May be slow on large tables but doesn't break reads |
| ⚠️ | Add a NOT NULL column | Requires a DEFAULT or backfill — old code doesn't set it |
| ⚠️ | Rename a column | Old code references the old name — must deploy code first |
| ❌ | Drop a column | Old code still SELECT/INSERT it — must remove code first |
| ❌ | Drop a table | Same — remove all references first |
Two-Phase Migration Pattern
For breaking schema changes, use two deployments:
Phase 1: Add the new column/table, update code to write to both old and new, read from new with fallback to old. Deploy.
Phase 2: Remove the old column/table references from code. Deploy. Then run a migration to drop the old column.
Migration File Naming
NNN_description.sqlSequential numbering, no gaps. Example: 018_add_organization_id_to_inboxes.sql
Manual Deployment Commands
If you need to deploy manually (outside CI):
# Build and push images
docker build --target api -t $ECR_REPO:api-latest .
docker build --target worker -t $ECR_REPO:worker-latest .
aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REPO
docker push $ECR_REPO:api-latest
docker push $ECR_REPO:worker-latest
# Force new deployment
aws ecs update-service --cluster mailman --service mailman-api --force-new-deployment
aws ecs update-service --cluster mailman --service mailman-worker --force-new-deployment
# Wait for stability
aws ecs wait services-stable --cluster mailman --services mailman-api mailman-workerRollback
ECS automatically rolls back if health checks fail during deployment. For manual rollback:
# List recent task definition revisions
aws ecs list-task-definitions --family mailman-api --sort DESC --max-items 5
# Pin to a previous revision
aws ecs update-service \
--cluster mailman \
--service mailman-api \
--task-definition mailman-api:PREVIOUS_REVISIONPre-Deployment Checklist
- [ ] All checks pass locally (
make check) - [ ] Migrations are backward-compatible (see safe migration checklist)
- [ ] If adding new config, ensure the env var is set in ECS task definition (Terraform)
- [ ] If adding new AWS resources,
terraform planshows expected changes - [ ] OpenAPI spec updated if API surface changed
SQLx Offline Mode
CI and Docker builds don't have database access. SQLx uses offline mode:
- Generate cache:
cargo sqlx prepare(writes.sqlx/directory) - Build with cache:
SQLX_OFFLINE=true cargo build
If you add or modify SQL queries, regenerate the cache before pushing:
cargo sqlx prepare -- --workspaceThe .sqlx/ directory is committed to the repo.