Safe Online Exam
Deploy the application

Docker Compose on Linux

Install or upgrade a verified Safe Online Exam release bundle on a Linux host with PostgreSQL, file secrets, optional Caddy TLS, cleanup, and backup safeguards.

The Compose release bundle runs the published application image, PostgreSQL 17, a one-shot migration service, and an opt-in cleanup service. It does not contain application source or any private SEB client identity material.

Use this path on a current, maintained Linux host with Docker Engine, Docker Compose v2, stable DNS, encrypted storage, time synchronization, monitored disk capacity, and restricted administrative access. Do not publish PostgreSQL. Canvas requires a stable public HTTPS origin.

Download and verify the release

export VERSION="X.Y.Z"
curl -fLO "https://github.com/JSB2010/safe-online-exam/releases/download/v${VERSION}/safe-online-exam-${VERSION}-compose.tar.gz"
curl -fLO "https://github.com/JSB2010/safe-online-exam/releases/download/v${VERSION}/safe-online-exam-${VERSION}-compose.tar.gz.sha256"
sha256sum --check "safe-online-exam-${VERSION}-compose.tar.gz.sha256"
tar -xzf "safe-online-exam-${VERSION}-compose.tar.gz"
cd "safe-online-exam-${VERSION}"

Read the extracted README.md, verify the published image attestation using the release-note command, and keep the exact bundled ghcr.io/jsb2010/safe-online-exam@sha256:... reference. Never replace it with a moving tag for production.

Guided installation

Run:

./setup.sh

The walkthrough creates and protects .env.secrets, asks whether bundled Caddy or an existing reverse proxy owns HTTPS, collects the public URL and Canvas values, generates database/session/LTI/certificate material, accepts the Canvas API secret without terminal echo, validates the rendered Compose model, and starts only after confirmation.

For automation, use the bundle's explicit non-interactive contract. Pass sensitive inputs through protected files, never command-line values:

./setup.sh \
  --non-interactive \
  --bootstrap \
  --no-caddy \
  --env-file .env.secrets \
  --canvas-api-client-secret-file /secure/input/canvas-api-secret

--configure-only validates the complete model without pulling or starting containers. Use --caddy only after setting PUBLIC_HOST and opening ports 80 and 443.

First-install Canvas bootstrap

Canvas cannot supply the LTI client and deployment IDs until it can read the running service configuration. On the first guided run, enter bootstrap-pending for both LTI values, but use the real final Canvas origin, public HTTPS URL, and API OAuth Developer Key.

After the stack and HTTPS are working:

  1. Create the LTI Developer Key using ${TOOL_URL}/lti/config.
  2. Install the external app and record its client and deployment IDs.
  3. Run ./setup.sh again and enter the real LTI values.
  4. Confirm /ready, load the Canvas theme loader, and complete role-based acceptance.

bootstrap-pending is a registration bootstrap value only. Signed LTI launches cannot work until the real values are saved and the app is recreated.

Runtime shape and HTTPS

The bundle starts PostgreSQL with a persistent postgres_data volume, a one-shot migrate service, the app, and an opt-in cleanup service. The application binds to loopback by default. Use an approved reverse proxy and set:

TOOL_URL=https://safe-online-exam.example.edu
CANVAS_REDIRECT_URI=https://safe-online-exam.example.edu/api/oauth2callback

The optional Caddy profile terminates public HTTPS when PUBLIC_HOST is set. Keep the app port and PostgreSQL private. Do not use a production HTTP TOOL_URL.

Cleanup and backups

Run cleanup at least daily from a monitored systemd timer or cron job:

docker compose \
  --env-file .env.secrets \
  -f compose.yaml \
  -f compose.secrets.yaml \
  --profile maintenance run --rm cleanup

Back up the database before high-risk changes and on the institution's documented schedule. A custom pg_dump is compressed but not encrypted; encrypt it immediately, move it to approved off-host storage, and remove the plaintext copy. Restore drills belong in a separate database or isolated host. Validate the archive, restore with --exit-on-error, run the intended migrations, inspect the application tables, and exercise an isolated flow. A backup that has never been restored is not verified.

Upgrade and rollback

Download and checksum the next bundle. Preserve .env.secrets, secrets/, the client identity record, and the database volume; merge new template keys. Then:

./upgrade.sh .env.secrets

The helper creates and validates a PostgreSQL custom-format backup, pulls the exact pinned images, applies forward migrations, restarts the topology, and checks readiness. Copy the backup to encrypted off-host storage.

Upgrade stored Canvas OAuth tokens from v1.0.x

Existing installations that have Canvas OAuth grants from before v1.1 must use the release bundle's two-stage token-encryption upgrade. Merge the new keyring and active-key settings from the bundle template, set OAUTH_TOKEN_ENCRYPTION_MODE=compat explicitly, and run the upgrade once. The bundle creates a new keyring only when one is absent; retain all existing protected settings.

Then set OAUTH_TOKEN_ENCRYPTION_MODE=enforce, recreate and verify the same image, and rewrite stored rows:

docker compose --env-file .env.secrets -f compose.yaml -f compose.secrets.yaml \
  --profile maintenance run --rm encrypt-oauth-tokens

Run the rewrite a second time and require zero rows updated before retiring any old key. Do not restart a pre-encryption image after encrypted token writes: it cannot read the new rows. The helper rejects a direct enforce upgrade when the preceding service has not already run in compat or enforce mode.

Application rollback does not reverse database migrations. Only restore an older image after confirming it supports the current schema. Recover data by restoring into a controlled target; never make automatic down-migration the response to a failed upgrade.

Complete the acceptance sequence after installation or any meaningful upgrade.

On this page