# Deploy the application

> Install a verified Safe Online Exam release on Cloud Run or Docker Compose, then preserve the migration, cleanup, backup, and recovery contract.

[Canonical HTML page](https://safeonlineexam.com/docs/deploy)

Safe Online Exam is distributed as a nonroot Node.js container. A supported
production installation also needs PostgreSQL 17 or newer, a stable public HTTPS
origin, protected configuration, a migration job before new traffic, scheduled
cleanup, backups with restore drills, monitoring, and a tested Canvas/SEB
workflow.

For a new public installation, use a published GitHub Release bundle rather than
a source checkout. The bundle is version-matched to an immutable image digest and
contains the installer, upgrade helper, and self-contained command reference for
that exact release.

## Verify the release before installation [#verify-the-release-before-installation]

Stable images are published at:

```text
ghcr.io/jsb2010/safe-online-exam
```

Stable releases publish `X.Y.Z`, `X.Y`, `X`, and `latest` tags, plus a
multi-architecture (`linux/amd64` and `linux/arm64`) manifest, SBOM, provenance,
GitHub attestation, and both deployment bundles. Tags help discovery; production
must use the exact `@sha256:...` digest recorded in the GitHub Release.

<Steps>
  <Step>
    ### Review and download the exact release [#review-and-download-the-exact-release]

    Read the changelog and release notes. Download the bundle and its adjacent
    `.sha256` file from
    [the releases page](https://github.com/JSB2010/safe-online-exam/releases).
  </Step>

  <Step>
    ### Verify the archive [#verify-the-archive]

    Run the release-note checksum command before extraction. For example:

    ```bash
    sha256sum --check "safe-online-exam-X.Y.Z-compose.tar.gz.sha256"
    ```
  </Step>

  <Step>
    ### Verify the image provenance [#verify-the-image-provenance]

    Use the exact `gh attestation verify` command printed in the release notes. It
    binds the digest to `JSB2010/safe-online-exam`, the release workflow, source
    commit, and tag.
  </Step>
</Steps>

Publishing a GitHub Release never deploys an institution's service automatically.
Record the release version, digest, verification result, and approver in the
institution's change record.

## Choose an operating model [#choose-an-operating-model]

<Cards>
  <Card title="Google Cloud Run + Cloud SQL" href="/docs/deploy/google-cloud" description="Recommended managed path. The bundle can provision reviewed Cloud SQL, uses Secret Manager version pins, stages a candidate revision with no traffic, and schedules cleanup." />

  <Card title="Docker Compose on Linux" href="/docs/deploy/docker" description="A maintained self-hosted path with PostgreSQL, migrations, file-secret bootstrap, optional Caddy TLS, backup, cleanup, and upgrade helpers." />
</Cards>

Other container platforms can run the image, but their manifests are not
maintained here. They must preserve the same contract: immutable image digest,
PostgreSQL 17+, protected runtime values, a pre-traffic migration job, scheduled
`db:cleanup --drain`, `/ready` gating, stable HTTPS, backups, monitoring, and
schema-aware rollback.

## First-install order [#first-install-order]

The two maintained bundles deliberately stop for Canvas at the points where Canvas
must supply values:

1. Verify the release and start the guided installer.
2. Create or validate PostgreSQL, protected values, the stable URL, and the SEB
   client identity (when using certificate encryption).
3. Create the Canvas API OAuth Developer Key for the final callback URL.
4. Let the installer deploy a first revision using `bootstrap-pending` LTI IDs.
5. Create the Canvas LTI Developer Key from `${TOOL_URL}/lti/config`, install the
   app, and record the real client and deployment IDs.
6. Finalize those IDs, load the Canvas theme loader, deploy the client identity
   where applicable, and complete end-to-end acceptance.

See [Setup overview](/docs/setup-overview) for the full handoff checklist and
[Canvas setup](/docs/canvas) for the Canvas portion.

## Invariants after go-live [#invariants-after-go-live]

* Run a migration from the exact new image before it receives traffic.
* Run bounded cleanup at least daily; expiry is a cleanup job, not database TTL.
* Keep public app and database ports private behind the approved HTTPS ingress.
* Preserve the version, digest, migration result, secret-version record, and
  release notes for every deploy.
* Test Classic Quiz, New Quiz, administrator, instructor, student, detector,
  certificate decryption when enabled, Config Key proof, tools, and exit after a
  meaningful change.
* Take and validate a backup before a high-risk upgrade. An application rollback
  does not reverse PostgreSQL migrations.

Next: [Google Cloud Run](/docs/deploy/google-cloud) or
[Docker Compose](/docs/deploy/docker).