Installing the platform on a new Ubuntu server¶
This guide takes a fresh Ubuntu LTS server to a running Quiz instance: Caddy in front (TLS), the application and PostgreSQL in Docker Compose, and, optionally, the code runner and the GitHub App. It follows the files of this repository; the deployment runbook describes how this project's own instance runs them, and is a useful companion.
Throughout, <your-host> is your instance's public host name (for
example quiz.example.org).
1. What you need¶
- A server with Ubuntu LTS, a public address, and SSH access as a user with sudo. 2 GB of memory is enough for a small instance.
- A DNS record (A/AAAA, or a CNAME) pointing
<your-host>at the server. - An OpenID Connect provider for sign-in, with a client registered for
the redirect URI
https://<your-host>/app/auth/callback. Any standard provider works: an institutional one (Switch edu-ID, for example), Keycloak, Microsoft Entra ID, Google… The client authenticates either with a client secret or with a private key (private_key_jwt). - A container image of the application (step 4).
2. Packages¶
Install git, curl, jq, Caddy and Docker Engine with the Compose plugin. Caddy and Docker publish their own apt repositories: follow Caddy's "Debian, Ubuntu, Raspbian" install instructions and Docker's "Install Docker Engine on Ubuntu", then:
sudo apt-get update && sudo apt-get install -y git curl jq
sudo apt-get install -y caddy
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Rootful Docker, as installed above, is the simplest. Running the stack in a dedicated account's rootless Docker, as this project does, is described in deployment §2; the commands below are the same, except for file ownership.
3. The checkout and the environment file¶
Run the stack from a checkout of the repository, by a dedicated user (here
quiz, in the docker group):
sudo useradd --create-home --shell /bin/bash --groups docker quiz
sudo -iu quiz sh -c 'git clone https://github.com/heig-tin-info/heig-quiz.git quiz && cd quiz \
&& mkdir -p secrets backups assets backup-status && chmod 755 backup-status \
&& cp .env.prod.example .env.prod && chmod 600 .env.prod'
Put the files the application reads (the OIDC private key, if any) in
~quiz/quiz/secrets/, then hand secrets/, assets/ and backups/ to the
container's user, node (uid 1000 inside the image, the same uid on the
host with rootful Docker):
sudo chown -R 1000:1000 ~quiz/quiz/secrets ~quiz/quiz/assets ~quiz/quiz/backups
sudo chmod 600 ~quiz/quiz/secrets/*
Fill .env.prod (every variable is explained in the file and in
the configuration reference):
POSTGRES_PASSWORDandCOOKIE_SECRET:openssl rand -base64 32each.SUPER_ADMIN_EMAIL: your address. The super administrator adds the teachers from Administration.- OIDC:
OIDC_ISSUER(your provider's issuer URL) andOIDC_CLIENT_ID; then eitherOIDC_CLIENT_SECRET, or, forprivate_key_jwt, the key insecrets/withOIDC_PRIVATE_KEY_PATH(secrets/<file>.pem) andOIDC_PRIVATE_KEY_KID(thekidof the key registered with the provider). Delete the example's edu-ID lines that do not apply. - The runner (step 6):
RUNNER_MODE=stubto start without it. - E-mail (optional): the platform sends e-mail through Scaleway
Transactional Email, configured by
SCW_SECRET_KEY,SCW_DEFAULT_PROJECT_ID,MAIL_FROM,MAIL_FROM_NAMEandMAIL_REGION. Left empty, every e-mail is written to the log instead of being sent. It is the only mail transport the platform implements. - Microsoft Teams, the LLM gateway and the kiosk stations stay off while their variables are empty (Microsoft Teams setup).
config.ts refuses to start under NODE_ENV=production on a development
login, an embedded (PGlite) database, the stub LLM or a placeholder
secret: if the application does not start, its log says which variable.
PostgreSQL is not installed on the host: it is the postgres service of
compose.prod.yml (PostgreSQL 17, a named volume), with a backup service
that dumps it daily into backups/.
4. Your host name and your image¶
Three values of the committed files are this project's; set yours:
| File | Line | Change |
|---|---|---|
Caddyfile |
8, quiz.chevallier.io { |
<your-host> { |
compose.prod.yml |
24, PUBLIC_URL: https://quiz.chevallier.io |
PUBLIC_URL: https://<your-host> |
compose.prod.yml |
19, image: ghcr.io/heig-tin-info/quiz:${IMAGE_TAG:-latest} |
your image |
The application listens on 127.0.0.1:3002 (compose.prod.yml line 34,
Caddyfile lines 23 and 30); change all three together if the port is
taken.
The image is built from the repository's Dockerfile (the API and the
built web app in one image, migrations applied at start). Build it on a
machine with a few GB of memory, or in CI, and push it to a registry your
server can pull from:
Building on a small server works but is slow and may run it out of memory.
This project builds in GitHub Actions and pushes to GitHub's registry
(deployment §4); a fork can reuse
that workflow with its own registry. If the registry is private, log in
once on the server (docker login <registry>).
5. Caddy and TLS¶
Caddy obtains and renews the certificate for <your-host> on its own once
the DNS record resolves and ports 80 and 443 reach the server.
# The default Caddyfile becomes one import line; each site is a fragment.
echo 'import /etc/caddy/conf.d/*.caddy' | sudo tee /etc/caddy/Caddyfile
sudo mkdir -p /etc/caddy/conf.d && sudo cp ~quiz/quiz/Caddyfile /etc/caddy/conf.d/quiz.caddy
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile && sudo systemctl reload caddy
On a server that already serves other sites, add the import line to the
existing Caddyfile instead of replacing it. The fragment proxies to the
application, streams /app/api/events unbuffered (the real-time channel)
and sets the transport headers; the application sends its own
Content-Security-Policy.
6. The code runner (optional)¶
Code questions run in a separate, hardened service, best on its own
machine with rootful Podman (deployment §3
and apps/runner/README.md).
In apps/runner/deploy/Caddyfile, set your runner's host name (the site
address) and your application server's addresses (remote_ip). Then, in
.env.prod: RUNNER_MODE=http, RUNNER_URL=https://<runner-host>:8443
and the same RUNNER_TOKEN (openssl rand -hex 32) as the runner's
/etc/quiz-runner/env.
Without it, RUNNER_MODE=stub: code questions stay authorable and
playable, their grading is proposed for a manual review, and /healthz
reports the runner as disabled
(without the runner).
7. First start¶
As quiz (sudo -iu quiz), in ~/quiz:
C="docker compose -f compose.prod.yml --env-file .env.prod"
$C pull app # or skip it for an image built on this server
$C up -d
$C logs --tail 50 app
The application applies the database migrations at start, then serves the
web app. IMAGE_TAG=<tag> in the environment (or in a second env file, as
deploy.sh does) selects a tag other than latest. deploy.sh and the
CI deploy jobs are this project's own pipeline (forced-command SSH keys,
promotion by commit sha), adaptable to yours
(deployment §4 and §5).
Do not seed a real instance: pnpm seed writes the development demo
world. Sign in with the super administrator's account and add teachers
from Administration.
8. Check it¶
curl -s -H "Authorization: Bearer $METRICS_TOKEN" https://<your-host>/healthz | jq . # 200: "database": "up", "jobs": "up"
cat backup-status/last.json # after the first dump: "ok": true
Then sign in and open Administration → System status. (pnpm smoke
needs the development login, which production refuses: it is not for a
real instance.) Point an uptime monitor at /healthz
(deployment §7), and copy backups/ off the
server regularly (deployment §6).
9. The GitHub App (optional)¶
For projects and GitHub-backed journals, create the instance's own GitHub
App with pnpm github:app from a workstation, copy its key and its six
GITHUB_* lines to the server, restart, and install the App on the
organizations it serves: Quiz's GitHub App. Without it,
the GitHub features are off and the rest of the platform runs.
10. A staging environment (optional)¶
A second stack beside the first, restored from production's data, is how
this project tests a change before it reaches students:
deployment §8
(compose.staging.yml, scripts/staging-refresh.sh). It needs its own
GitHub App, under the staging rule.