> ## Documentation Index
> Fetch the complete documentation index at: https://docs.safeonlineexam.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Docker Compose Deployment: Safe Online Exam on Linux

> Install Safe Online Exam on a Linux host with Docker Compose, covering bundle verification, guided setup, TLS proxy, backups, and the upgrade helper.

Safe Online Exam's Docker Compose release bundle provides a complete self-hosted installation path for Linux servers with Docker Engine and the Compose v2 plugin. This page covers downloading and verifying the release bundle, running the guided installer, configuring TLS, scheduling backups and cleanup, and upgrading to a new release using the bundle's upgrade helper.

<Note>
  The application binds to `127.0.0.1:8080` by default. Canvas requires a stable public HTTPS origin, so you must place a TLS reverse proxy (Caddy, nginx, Traefik, or a managed ingress) in front of the application before creating any Canvas registrations. The bundle includes an optional Caddy profile for servers where ports 80 and 443 are available.
</Note>

## What the Bundle Contains

The Compose release bundle includes:

* The exact application image digest for the release.
* PostgreSQL 17 with a named `postgres_data` volume.
* A one-shot migration service that gates application startup.
* An opt-in cleanup service for expired sessions and one-time state.
* Mounted file secrets (no plaintext values in `compose.yaml`).
* Protected LTI and SEB identity bootstrap via `setup.sh`.
* A backup and upgrade helper (`upgrade.sh`).
* An optional Caddy HTTPS profile.

## Host Requirements

Use a supported Linux distribution with current security updates and the following:

* Docker Engine with the Compose v2 plugin installed.
* Stable DNS, encrypted storage, and time synchronization.
* Monitored disk capacity — the PostgreSQL volume grows with usage.
* SSH access restricted to administrative networks.
* Port 5432 **not** published to the public internet.

## Installation

<Steps>
  <Step title="Download and verify the release bundle">
    Replace `X.Y.Z` with the target release version. Verify the checksum before extraction.

    ```bash theme={null}
    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` before proceeding. It contains release-specific notes that take precedence over this general guide.
  </Step>

  <Step title="Run the guided installer">
    The `setup.sh` script creates a protected `.env.secrets` file, a `secrets/` directory with mounted file secrets, and the client-only SEB identity. It prompts for required values through no-echo input — do not pass secrets as command-line arguments.

    ```bash theme={null}
    ./setup.sh
    ```

    For a manual source-topology install, protect the environment file instead:

    ```bash theme={null}
    cp .env.compose.secrets.example .env
    chmod 600 .env
    ```

    Prefer the bundle's generated file-secret topology (`secrets/` directory with mounted files) over placing production secret values directly in the environment file.
  </Step>

  <Step title="Configure your TOOL_URL and Canvas redirect URI">
    Before adding Canvas registrations, set the public HTTPS origin in your protected environment file. These values must be final before you create any Canvas Developer Keys.

    ```text theme={null}
    TOOL_URL=https://safe-online-exam.example.edu
    CANVAS_REDIRECT_URI=https://safe-online-exam.example.edu/api/oauth2callback
    ```

    If you are using the optional bundled Caddy profile, also set `PUBLIC_HOST` to the same hostname and ensure ports 80 and 443 are available on the host.
  </Step>

  <Step title="Configure and start the application">
    Start the full topology with your environment file and both compose files:

    ```bash theme={null}
    docker compose \
      --env-file .env.secrets \
      -f compose.yaml \
      -f compose.secrets.yaml \
      up -d
    ```

    The migration service runs first and gates application startup. Check that it completed successfully before proceeding:

    ```bash theme={null}
    docker compose --env-file .env.secrets -f compose.yaml -f compose.secrets.yaml logs migrate
    ```
  </Step>

  <Step title="Configure TLS (optional Caddy profile)">
    If you are using the bundled Caddy profile, activate it alongside the base compose files. Caddy automatically obtains and renews a TLS certificate using ACME (Let's Encrypt or ZeroSSL) once `PUBLIC_HOST` is set and ports 80/443 are accessible.

    For any other reverse proxy (nginx, Traefik, or a managed ingress), terminate TLS externally and proxy `https://<your-domain>` to `127.0.0.1:8080`. Keep the application and PostgreSQL ports private.
  </Step>

  <Step title="Register the tool in Canvas">
    With the service running and reachable at its public HTTPS origin, proceed to the [Canvas Setup](/deployment/canvas-setup) guide to create the API OAuth Developer Key and the LTI 1.3 Developer Key. Return here to deploy the final service revision with `LTI_CLIENT_ID` and `LTI_DEPLOYMENT_ID` after Canvas registration.
  </Step>
</Steps>

## Scheduling Cleanup

Run the maintenance profile at least once daily. Use a systemd timer, cron job, or your host's scheduler:

```bash theme={null}
docker compose \
  --env-file .env.secrets \
  -f compose.yaml \
  -f compose.secrets.yaml \
  --profile maintenance run --rm cleanup
```

Use absolute paths in unattended schedulers, rotate the scheduler's logs, and alert when executions stop.

## Backups and Restore Drill

The named `postgres_data` volume provides data durability across container restarts but is **not** a substitute for a database backup. Use `pg_dump` inside the PostgreSQL container to create a compressed, custom-format backup:

```bash theme={null}
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 > safe-online-exam.dump
```

<Warning>
  The dump is compressed but not encrypted. Encrypt it immediately with your institution's approved tool, move it to access-controlled off-host storage, and remove the plaintext copy from the local filesystem.
</Warning>

Perform a restore drill into a separate database or isolated host on a regular schedule:

1. Validate the archive with `pg_restore --list`.
2. Restore with `pg_restore --exit-on-error`.
3. Run the intended image's migrations against the restored database.
4. Inspect all application tables.
5. Exercise an isolated LTI/assessment flow.
6. Record recovery time and the newest restored row.

A backup that has never been restored is not verified.

## Upgrade Procedure

<Steps>
  <Step title="Download and verify the new bundle">
    Download the next release bundle and verify its checksum following the same steps as the initial install. Do not overwrite your existing installation directory.

    ```bash theme={null}
    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"
    ```
  </Step>

  <Step title="Run the upgrade helper with the existing environment path">
    Keep your protected environment file, secret files, client identity record, and database volume in a durable installation directory. Invoke the new bundle's `upgrade.sh` with the path to the existing environment file:

    ```bash theme={null}
    NEW_BUNDLE=/opt/safe-online-exam-X.Y.Z
    EXISTING_ENV=/srv/safe-online-exam/.env.secrets
    "$NEW_BUNDLE/upgrade.sh" "$EXISTING_ENV"
    ```

    The helper:

    * Creates a PostgreSQL custom-format backup and validates it.
    * Pulls the exact pinned images from the new bundle.
    * Applies forward migrations.
    * Restarts the topology with the new image.
    * Verifies readiness before reporting success.

    Relative secret and backup paths resolve from the directory containing the existing environment file.
  </Step>

  <Step title="Copy the backup to encrypted off-host storage">
    The upgrade helper creates a pre-upgrade backup in the installation directory. Copy it to encrypted, access-controlled off-host storage before continuing.
  </Step>

  <Step title="Verify the upgrade">
    Confirm the application is healthy and that the new revision is serving traffic:

    ```bash theme={null}
    curl -fsS "${TOOL_URL}/health"
    curl -fsS "${TOOL_URL}/ready"
    curl -fsS "${TOOL_URL}/.well-known/jwks.json"
    curl -fsS "${TOOL_URL}/lti/config"
    ```

    Then complete a real Canvas and SEB acceptance test before the next high-stakes assessment period.
  </Step>
</Steps>

<Note>
  Application rollback does not undo database migrations. Restore an older image only after confirming it supports the current schema. Data rollback requires a reviewed restore into a controlled target.
</Note>

### OAuth Token Encryption Upgrade (Existing Installations)

If upgrading from a version that used plaintext OAuth tokens, use the staged transition:

1. Upgrade with `OAUTH_TOKEN_ENCRYPTION_MODE=compat` in `.env.secrets`. The helper generates the new `secrets/oauth_token_encryption_keyring` file if absent and never changes an existing keyring.
2. After the compat revision is healthy, change the mode to `enforce` and recreate the application container.
3. Run the token rewrite job:

```bash theme={null}
docker compose --env-file .env.secrets -f compose.yaml -f compose.secrets.yaml \
  --profile maintenance run --rm encrypt-oauth-tokens
```

Run the job a second time and confirm `0` updates before removing any retired key. Fresh installations start directly in `enforce` mode.

## Next Steps

After the service is running and healthy, proceed to [Canvas Setup](/deployment/canvas-setup) to register the LTI and OAuth Developer Keys, then provision the SEB encryption identity following the [Certificate Management](/deployment/certificate-management) guide.
