Safe OnlineExam
Roll out & operate

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:

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=10

Restore 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-protection

Connect 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-acl

Do 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 --wait

Migration 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_IMAGE and run the same up --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_connections

The 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" | head

Then 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.

On this page