Docker / VPS
Run the same container on any Linux host with Docker Compose — host prep, mounted secrets, the migration gate, a TLS reverse proxy, and the two-pass bootstrap.
This mode runs the same production image on any provider that gives you a persistent Linux host with Docker. You operate DNS, TLS, host security, backups, monitoring, and upgrades. The checked-in Compose topology includes the application, PostgreSQL, a one-shot migration job, an opt-in cleanup job, and a named data volume.
Use the mounted-secrets variant
These steps use .env.compose.secrets.example and the compose.secrets.yaml
override so application secrets are mounted as files rather than stored as plain
Compose environment values. This is the recommended posture for a real host.
Prepare the host
Use a supported Linux distribution with current security updates, Docker Engine,
the Docker Compose v2 plugin, and age (or another approved backup-encryption
tool). Then configure:
- a stable DNS name pointing at the host;
- inbound TCP 80 and 443 for certificate issuance and HTTPS;
- SSH only from trusted administrative networks;
- no public PostgreSQL port;
- encrypted disks, automatic security updates, time sync, and off-host monitoring.
docker version
docker compose version
git clone YOUR_REPOSITORY_URL seb-canvas-lti
cd seb-canvas-ltiBuild or select an image
Build locally with an immutable tag, or pull a trusted published image by digest. Never use an unpinned moving tag in production.
export APP_IMAGE="canvas-seb-lti:$(git rev-parse --short=12 HEAD)"
docker build --tag "$APP_IMAGE" .
# or, by digest:
# export APP_IMAGE="ghcr.io/your-organization/seb-canvas-lti@sha256:REPLACE"
# docker pull "$APP_IMAGE"The same image runs the application, migrations, and cleanup.
Generate keys and prepare secrets
umask 077
cp .env.compose.secrets.example .env.secrets
chmod 600 .env.secrets
mkdir -p secrets
chmod 700 secrets
node scripts/generate-lti-private-key.mjs production >secrets/lti_private_key
openssl rand -base64 48 >secrets/database_password
openssl rand -base64 48 >secrets/session_secret
openssl rand -base64 48 >secrets/state_encryption_key
printf '%s' 'your-canvas-api-client-secret' >secrets/canvas_api_client_secret
: >secrets/seb_quit_password
mkdir -p .local
openssl rand -base64 48 >.local/docker-seb-p12-password
SEB_CERT_NAME=seb-config-encryption \
SEB_CERT_SUBJECT="/CN=Safe Online Exam Configuration Encryption/O=Organization" \
bash scripts/generate-seb-config-cert.sh \
.local/docker-seb-certs \
.local/docker-seb-p12-password
cp .local/docker-seb-certs/seb-config-encryption.crt.pem \
secrets/seb-config-encryption.crt.pem
chmod 644 secrets/*Only the public certificate stays on the host
Move the .p12, the private key, and the passphrase off the host into your
device-deployment vault. The host keeps only
secrets/seb-config-encryption.crt.pem. See
Certificate management.
Edit .env.secrets and set at least:
APP_IMAGE— the exact tag or digest from Step 2;TOOL_URL— the final HTTPS origin;CANVAS_DOMAINand the exact OAuth callback;LTI_CLIENT_IDandLTI_DEPLOYMENT_ID— orbootstrap-pendingfor both during the first pass;CANVAS_API_CLIENT_ID;DATABASE_NAME=canvas_sebandDATABASE_USER=canvas_seb;APP_ENV=prod,NODE_ENV=production, and the hardened defaults already present.
Validate and start
Check the resolved configuration without printing secrets, then bring up the
stack. PostgreSQL starts, the one-shot migrate service runs, and app waits for
it to succeed.
docker compose \
--env-file .env.secrets \
-f compose.yaml \
-f compose.secrets.yaml \
config --quiet
docker compose \
--env-file .env.secrets \
-f compose.yaml \
-f compose.secrets.yaml \
up --detach --wait
curl -fsS http://127.0.0.1:8080/health
curl -fsS http://127.0.0.1:8080/readyThe topology has four roles: postgres (with the persistent postgres_data
volume, no published port), migrate (the one-shot gate), app (bound to
loopback), and cleanup (opt-in maintenance).
Add HTTPS with a reverse proxy
Keep the app on loopback and terminate TLS in front of it. Example Caddyfile:
seb.example.edu {
reverse_proxy 127.0.0.1:8080
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
}Set TOOL_URL=https://seb.example.edu and
CANVAS_REDIRECT_URI=https://seb.example.edu/api/oauth2callback, reload the proxy,
restart the app after any environment change, and verify the public origin:
curl -fsS https://seb.example.edu/health
curl -fsS https://seb.example.edu/ready
curl -fsS https://seb.example.edu/.well-known/jwks.json
curl -fsS https://seb.example.edu/lti/config
curl -fsS https://seb.example.edu/js/canvas-seb-detector.js | headDo not expose port 8080 directly, publish port 5432, or use a plain-HTTP
TOOL_URL.
Finish Canvas setup and recreate (pass 2)
Now that ${TOOL_URL}/lti/config is public, create and install the LTI Developer
Key (Connect Canvas). Replace the two bootstrap-pending values
in .env.secrets with the real client and deployment IDs, then recreate the app
through the full dependency gate:
docker compose \
--env-file .env.secrets \
-f compose.yaml \
-f compose.secrets.yaml \
up --detach --waitSchedule cleanup
Run cleanup at least daily — a root-owned systemd timer is preferred; cron is fine on a simple host. Test the exact command interactively first:
docker compose \
--env-file .env.secrets \
-f compose.yaml \
-f compose.secrets.yaml \
--profile maintenance run --rm cleanupExample root crontab entry with absolute paths:
17 3 * * * cd /opt/seb-canvas-lti && /usr/bin/docker compose --env-file .env.secrets -f compose.yaml -f compose.secrets.yaml --profile maintenance run --rm cleanup >>/var/log/canvas-seb-cleanup.log 2>&1Protect and rotate the cleanup log, and alert when the scheduled command fails or stops running.
A simpler, non-mounted start
For evaluation or a hardened host that does not require file-mounted secrets, the
project also supports a direct .env file:
docker build -t canvas-seb-lti:local .
cp .env.compose.example .env
chmod 600 .env
mkdir -p secrets
# set real values in .env, place the public cert at secrets/seb-config-encryption.crt.pem
APP_IMAGE=canvas-seb-lti:local docker compose up -d --wait
curl -fsS http://127.0.0.1:8080/healthCompose binds to loopback by default; still put a TLS reverse proxy in front before Canvas can reach it.
Day-2 operations
Encrypted backups and restore drills, upgrades, rollback, host recovery, and pool sizing are in Operations. Run the verification sequence before any real exam.
Next: Configuration reference · Certificate management · or jump to Connect Canvas.
Google Cloud Run
Full first-time provisioning on Google Cloud Run with Cloud SQL — APIs, identity, database, secrets, the stable URL, and the two-pass deploy.
Configuration reference
Every runtime setting — profiles and hardened validation, database, required application values, file-based secrets, Canvas/LTI endpoints, SEB options, and secret rotation.