Runbook: R940 LastVet production deploy¶
Replaces: AWS LASTVET_ALLOW_PROD_DEPLOY=1 gate in deploy-api-image.sh (ECS retired August 2026).
Canonical topology: docs/deployment/R940_ENVIRONMENT_TOPOLOGY.md.
When to use¶
Any change that affects production on R940:
- API code deploy (
lastvet-api-prod.servicerestart after build) - Database migrations against
lastvet_prod - BWS env restore to
/etc/lastvet-api/lastvet-api-prod.env - Provider portal production Cloudflare deploy (if bundled with API behavior change)
Not required for staging-only work, observability changes, or docs.
Pre-flight (mandatory)¶
- Staging soaked: Staging deploy completed and verified. Minimum 48 hours soak for non-trivial changes (consent, RLS, imaging, auth). Shorter window only for hotfix with Ryan approval.
- Staging health:
curl -sS https://api-staging.last.vet/health
ssh lastvet-api 'systemctl is-active lastvet-api-staging'
- Migration state verified (prod vs staging): Before any prod deploy that ships new migration files, confirm ledger parity and pending set (see Migration pre-flight checklist below). Do not restart prod or run prod migrations until this passes.
- Fixtures current: If the change touches consent/RLS/Part 2, confirm staging fixture veterans A/B/C present (see topology doc).
- No prod seeding: Confirm deploy plan does not run
seed-staging-fixtures.jsor any seed against prodDATABASE_APP_URL.
Migration pre-flight checklist (prod vs staging)¶
Run from agent-01 or any host with SSH to lastvet-db and lastvet-api. Read-only until the apply step in Deploy steps §3.
1. Compare ledger (latest applied)¶
ssh lastvet-db 'sudo -u postgres psql -d lastvet_prod -tAc "SELECT max(filename) FROM schema_migrations;"'
ssh lastvet-db 'sudo -u postgres psql -d lastvet_staging -tAc "SELECT max(filename) FROM schema_migrations;"'
Expect: Staging max filename ≥ prod max filename before prod promotion. If prod lags staging, migrations are in scope for this deploy.
2. Full diff (optional but recommended for hotfixes)¶
ssh lastvet-db 'sudo -u postgres bash -c "
psql -d lastvet_prod -tAc \"COPY (SELECT filename FROM schema_migrations ORDER BY filename) TO STDOUT\" > /tmp/prod.txt
psql -d lastvet_staging -tAc \"COPY (SELECT filename FROM schema_migrations ORDER BY filename) TO STDOUT\" > /tmp/staging.txt
echo \"=== In staging not prod ===\"
comm -23 /tmp/staging.txt /tmp/prod.txt
"'
Any file listed here must be applied on prod before or during this deploy.
3. List pending on prod (migrate runner)¶
lastvet_master is allowed from LAN only in pg_hba.conf. The restore script rewrites MIGRATION_DATABASE_URL to 192.168.1.91; if running manually, apply the same rewrite.
ssh lastvet-api 'sudo bash -c "
export LASTVET_ALLOW_R940_PROD_DEPLOY=1
set -a && source /etc/lastvet-api/lastvet-api-prod.env && set +a
export MIGRATION_DATABASE_URL=\"\${MIGRATION_DATABASE_URL//lastvet-db.tailc3bbdc.ts.net/192.168.1.91}\"
cd /opt/last.vet-api
node scripts/migrate.js --list-pending
"'
Expect: Empty, or exactly the migration set you intend to apply. The runner never auto-applies *_down.sql or files under migrations/rollback/ (manual rollback only).
4. Pin expected set before apply¶
When applying, use --expect so a surprise pending file aborts the run:
node scripts/migrate.js --expect 059_imaging_upload_types.sql,060_imaging_download_audit_action.sql,061_imaging_envelope.sql
5. Snapshot before prod schema change¶
On lastvet-db, before any prod migration:
SNAPSHOT="/var/backups/postgres/lastvet_prod_pre_migrate_$(date -u +%Y%m%dT%H%M%SZ).dump"
sudo mkdir -p /var/backups/postgres
sudo -u postgres pg_dump -Fc -d lastvet_prod -f "$SNAPSHOT"
ls -lh "$SNAPSHOT"
Confirm size is non-zero. Do not proceed without a verified snapshot.
Prod confirmation gate¶
Production deploy is blocked until the operator sets:
Meaning: intentional, reviewed production change. Unset or any other value = do not restart prod or run prod migrations.
Optional typed confirmation (recommended for migrations):
read -r -p "Type prod to confirm R940 production deploy: " confirm
[[ "$confirm" == "prod" ]] || { echo "Aborted."; exit 1; }
export LASTVET_ALLOW_R940_PROD_DEPLOY=1
Document in your session notes: date, git SHA, migration count, staging soak start time.
Deploy steps¶
Run on lastvet-api VM (or via ssh lastvet-api from agent-01).
1. Build¶
2. Restore prod env from BWS¶
Script validates required keys (DATABASE_APP_URL, VETERAN_JWT_SECRET, OAUTH_JWT_SECRET, SECURE_LINK_HMAC_SECRET, etc.). It does not print secret values.
3. Migrations (if any)¶
Pre-requisite: Migration pre-flight checklist completed; snapshot taken if applying.
set -a && source /etc/lastvet-api/lastvet-api-prod.env && set +a
# Gate check
[[ "${LASTVET_ALLOW_R940_PROD_DEPLOY:-}" == "1" ]] || {
echo "Prod migration blocked: set LASTVET_ALLOW_R940_PROD_DEPLOY=1" >&2
exit 1
}
# lastvet_master: pg_hba allows LAN source only (restore script sets 192.168.1.91)
export MIGRATION_DATABASE_URL="${MIGRATION_DATABASE_URL//lastvet-db.tailc3bbdc.ts.net/192.168.1.91}"
node scripts/migrate.js --list-pending
node scripts/migrate.js --expect <comma-separated-filenames> # required when applying
Never use staging DATABASE_APP_URL for prod migrations. Verify DATABASE_APP_URL host/database before running (grep key name only, do not log URL).
4. Restart prod service¶
[[ "${LASTVET_ALLOW_R940_PROD_DEPLOY:-}" == "1" ]] || {
echo "Prod restart blocked: set LASTVET_ALLOW_R940_PROD_DEPLOY=1" >&2
exit 1
}
sudo systemctl restart lastvet-api-prod
sudo systemctl status lastvet-api-prod --no-pager
journalctl -u lastvet-api-prod -n 50 --no-pager
5. Post-deploy verification¶
curl -sS https://api.last.vet/health
# Authenticated spot-check if change touches veteran or provider paths
ssh lastvet-api 'journalctl -u lastvet-api-prod -n 20 --no-pager | grep -i error || true'
Watch UptimeRobot / Grafana for LastVetAPIProdSilent and error-rate alerts (see docs/observability/runbooks/).
6. Unset gate¶
Rollback¶
git checkout <previous-good-sha>in/opt/lastvet/last.vet-api, rebuild, restart prod (still requiresLASTVET_ALLOW_R940_PROD_DEPLOY=1).- Database: migrations are forward-only unless a dedicated down migration exists. Do not restore prod DB from staging.
- DNS: prod API is Cloudflare → cloudflared → R940. AWS ALB rollback is historical only (stacks decommissioned). See migration plan rollback section if dual-stack window still open.
Never¶
- Restart
lastvet-api-prodwithout explicitLASTVET_ALLOW_R940_PROD_DEPLOY=1 - Run fixture or seed scripts against prod
- Copy staging env file to prod path
- Blind-sync local
.envinto BWS prod keys
Resolution history¶
| Date | Notes |
|---|---|
| 2026-08-18 | Migration pre-flight checklist; LAN host for prod migrate; snapshot gate; --expect required when applying |
| 2026-08-18 | Initial R940 runbook; replaces AWS LASTVET_ALLOW_PROD_DEPLOY discipline |