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

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

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 [#download-and-verify-the-release]

```bash
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 [#guided-installation]

Run:

```bash
./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:

```bash
./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 [#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 [#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:

```text
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 [#cleanup-and-backups]

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

```bash
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 [#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:

```bash
./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.

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](/docs/operate) after installation or any
meaningful upgrade.