Operations
Day-2 running of the service — cleanup, backups and restore drills, upgrades, application vs schema rollback, pool sizing, monitoring, and the dev database reset.
Once the integration is verified, this is how you keep it healthy. The invariants from Deployment overview apply throughout — in particular, application rollback and schema recovery are separate decisions.
Scheduled cleanup
Expired sessions, one-time state, and locks are drained by a bounded cleanup job. Run it at least daily. You configured it during deploy:
- Google Cloud — a Cloud Scheduler job invokes the Cloud Run cleanup job. See Google Cloud → Schedule cleanup.
- Docker / VPS — a systemd timer or cron entry runs the
cleanupCompose service. See Docker → Schedule cleanup.
Alert if the scheduled cleanup fails or stops running — a stalled cleanup lets transient tables grow unbounded.
Backups and restore drills
A backup is not verified until it restores
Always restore into a separate drill target, never the live instance. Record recovery time and the newest restored row timestamp, then discard the drill.
Keep automated backups and point-in-time recovery on. Take an on-demand backup before a high-risk release:
gcloud sql backups create \
--instance="$SQL_INSTANCE" \
--description="pre-release $(date -u +%Y-%m-%dT%H:%M:%SZ)"
gcloud sql backups list --instance="$SQL_INSTANCE" --limit=10Restore drills target a separate instance from the newest backup:
export BACKUP_ID="$(gcloud sql backups list \
--instance="$SQL_INSTANCE" --sort-by='~endTime' --limit=1 \
--format='value(id)')"
export RESTORE_INSTANCE="${SQL_INSTANCE}-restore-drill"
gcloud sql backups restore "$BACKUP_ID" \
--backup-instance="$SQL_INSTANCE" \
--restore-instance="$RESTORE_INSTANCE" \
--region="$REGION" \
--edition=ENTERPRISE --cpu=1 --memory=3840MiB \
--no-deletion-protectionConnect only an isolated migration/readiness test to the drill instance, then delete it once approved. Never point the live Cloud Run service at a restore drill.
The Compose database has no host port, so dump inside the container. The custom dump format is compressed but not encrypted — pipe it straight into your encryption tool and copy off-host.
umask 077
mkdir -p backups
export AGE_RECIPIENT="age1REPLACE_WITH_RECOVERY_RECIPIENT"
export BACKUP_FILE="backups/canvas-seb-$(date -u +%Y%m%dT%H%M%SZ).dump.age"
docker compose --env-file .env.secrets -f compose.yaml -f compose.secrets.yaml \
exec -T postgres \
pg_dump --username=canvas_seb --dbname=canvas_seb \
--format=custom --no-owner --no-acl \
| age --recipient "$AGE_RECIPIENT" >"$BACKUP_FILE"Restore only into a new drill database, then run migrations against it and exercise an isolated smoke flow before approving:
docker compose --env-file .env.secrets -f compose.yaml -f compose.secrets.yaml \
exec -T postgres createdb --username=canvas_seb canvas_seb_restore_drill
age --decrypt --identity /secure/path/age-identity.txt "$BACKUP_FILE" \
| docker compose --env-file .env.secrets -f compose.yaml -f compose.secrets.yaml \
exec -T postgres \
pg_restore --username=canvas_seb --dbname=canvas_seb_restore_drill \
--exit-on-error --no-owner --no-aclDo not treat a raw volume copy taken while PostgreSQL is running as a verified backup.
Upgrades
A normal upgrade is the same Cloud Build submission. It reuses cached layers, verifies the new image, runs PostgreSQL integration tests, deploys and waits for migrations, then creates a new revision at the existing URL. Run the smoke checks afterward.
Build or pull the new immutable image, update APP_IMAGE in .env.secrets, take a
backup, and bring the stack up. Compose recreates the migration service for the new
image and keeps the app behind its success condition.
docker build --tag canvas-seb-lti:NEW_VERSION .
# edit APP_IMAGE in .env.secrets
docker compose --env-file .env.secrets -f compose.yaml -f compose.secrets.yaml \
up --detach --waitMigration checksum validation fails if an applied migration was edited — add a new forward migration instead of editing history.
Rollback
Confirm schema compatibility first
Before rolling back the application, confirm the older revision supports the current schema. Traffic rollback does not roll back PostgreSQL.
-
Google Cloud — list revisions and shift traffic only when compatibility is confirmed:
gcloud run revisions list --service="$SERVICE" --region="$REGION" gcloud run services update-traffic "$SERVICE" --region="$REGION" \ --to-revisions="PREVIOUS_REVISION=100" -
Docker / VPS — restore the prior immutable
APP_IMAGEand run the sameup --detach --wait. This rolls back the application only.
Schema recovery means a reviewed forward migration or a restore into a controlled target — never an automatic down-migration during a failed deploy. Use expand/migrate/deploy/contract changes so adjacent revisions stay compatible.
PostgreSQL pool sizing
DATABASE_POOL_MAX is per application process. Reserve connections for migrations,
cleanup, administration, monitoring, and restore work:
(max app instances × DATABASE_POOL_MAX) + job/admin reserve < database max_connectionsThe checked-in Cloud Run configs use a pool max of 5; production's ten instances therefore use at most 50 application connections before the reserve. Recalculate before changing either value.
Secret rotation
Rotate one secret at a time: create a new immutable version, point the runtime at
it, deploy, smoke test, then disable the old version. Consequences (sessions
invalidated, outstanding state invalidated, Canvas/JWKS coordination, fresh
.seb downloads) are in
Configuration → Secret rotation.
Release smoke checks
Run after every release:
curl -fsS "${TOOL_URL}/health"
curl -fsS "${TOOL_URL}/ready"
curl -fsS "${TOOL_URL}/.well-known/jwks.json"
curl -fsS "${TOOL_URL}/lti/config"
curl -fsS "${TOOL_URL}/js/canvas-seb-detector.js" | head
curl -fsS "${TOOL_URL}/js/canvas-seb-theme-loader.js" | headThen complete the verification sequence for any change that touches authentication, Canvas, settings, configuration, the detector, or exit.
Monitoring
Alert on: application 5xx responses, /ready failures, migration/cleanup job
failures, PostgreSQL connection saturation, database storage growth, failed
backups, stale cleanup executions, and certificate expiry. On a VPS, also watch
disk space, container health, backup age, and host security updates.
Development database reset (dev only)
A guarded command exists for a completely fresh dev state. It permanently destroys all records in the maintained dev database before recreating it and reapplying migrations.
npm run db:reset:gcloud:dev -- --project "$PROJECT_ID"Read and confirm the displayed project, instance, and database before accepting the prompt. Never run it against production, never place it in CI or a scheduler, and take an on-demand backup first if the current dev state might be needed.
Student experience
What a student does — connect Canvas once, run a setup check, download a fresh configuration, and take the exam in SEB — and what the tool does for them.
Incident response
What to do for a bad release, a secret or certificate compromise, and host loss — without weakening the security controls under pressure.