Skip to content

Deployment

A single docker-compose.yml runs the whole stack:

Service Stack Default port
frontend SvelteKit (Bun) 3000 (loopback only)
backend Ktor (Kotlin, fat JAR on JRE) 8088 (loopback only)
db Postgres 5432 (loopback only)
docs This site (Starlight on nginx) 3001

You need Docker Engine with the Compose plugin and a checkout of the Hanami repository. The first build downloads the application dependencies and may take several minutes.

Terminal window
cp .env.example .env
# Set POSTGRES_PASSWORD, FRONTEND_SESSION_ENCRYPTION_KEY, and
# FRONTEND_SERVICE_SECRET in .env.
docker compose up --build

Open http://localhost:3000 and sign in with an A+ API token (or via LTI once configured).

Check the containers and follow application logs if the page does not load:

Terminal window
docker compose ps
docker compose logs --tail=100 frontend backend db

POSTGRES_PASSWORD is deliberately without a default: docker compose up fails fast while it is empty, so the database is never brought up with a world-known credential.

Compose starts the frontend after /health/ready confirms that the backend can query its database. /health/live reports process liveness. /metrics exposes Prometheus metrics for Ktor, the JVM, and the Postgres connection pool. Keep the backend port private or restrict access to /metrics at the reverse proxy.

The frontend port is also loopback-only by default. To reach Hanami directly from another device, or from a reverse proxy on another host, set FRONTEND_BIND=0.0.0.0. Restrict that port to the intended clients with the host firewall. A proxy on the same host can keep the safer loopback default.

A deployment talks to exactly one A+ instance, set with APLUS_BASE_URL in .env (default: the production Aalto A+, https://plus.cs.aalto.fi/api/v2/). Courses on that instance are configured in the database by teachers; see Course configuration.

Set APLUS_URL as well when links opened by a user’s browser need a different origin from the backend API. This is common in local development, where the backend uses http://plus:8000/api/v2/ but the browser uses http://localhost:8000. SvelteKit validates this runtime value at startup and uses it for LTI form-post protection. Changing it requires restarting the frontend process, not rebuilding the image.

The optional edge profile runs Caddy as the supported production reverse proxy. Caddy obtains and renews the TLS certificate, redirects HTTP to HTTPS, serves HTTP/2 and HTTP/3, compresses responses, and forwards streamed frontend responses without buffering them.

Point the hostname’s public A/AAAA record at the server and allow inbound TCP 80, TCP 443, and UDP 443. Then configure .env:

HANAMI_HOST=hanami.example.com
FRONTEND_ORIGIN=https://hanami.example.com
PROTOCOL_HEADER=x-forwarded-proto
HOST_HEADER=x-forwarded-host
ADDRESS_HEADER=x-forwarded-for
XFF_DEPTH=1

Start the stack with the edge profile:

Terminal window
docker compose --profile edge up -d --build

The frontend’s direct port is bound to host loopback, so public traffic cannot bypass Caddy. The caddy-data and caddy-config volumes persist certificates and Caddy state across container replacement. docker compose ps reports Caddy’s health from its local admin endpoint. The ordinary docker compose up command still runs the stack without the optional edge for local use.

Caddy sends only /api/v1 and its descendants directly to Ktor. Browser pages go to SvelteKit. /internal/frontend, manual-grading routes, health, metrics, /api/v10, and unrelated /api paths are not exposed as Ktor routes at the public edge.

Verify the deployment from another machine:

Terminal window
curl --http2 -I https://hanami.example.com
curl --http3-only -I https://hanami.example.com

The first response should report HTTP/2. The second requires a curl build with HTTP/3 support and should report HTTP/3; UDP 443 must be reachable. Also verify that http://hanami.example.com redirects to HTTPS, a normal sign-in works, a large bounded submission preview loads, its archive download streams, and deferred review content appears without waiting for the entire page response.

If an existing external proxy is used instead, preserve the same forwarded headers, disable response buffering, keep compression enabled, and restrict the frontend port from public access. A proxy on another host also needs FRONTEND_BIND=0.0.0.0. Set COOKIE_SECURE=true if that proxy cannot provide the scheme header.

  • Set a unique POSTGRES_PASSWORD and keep .env readable only by the deployment account.
  • Generate independent values for FRONTEND_SESSION_ENCRYPTION_KEY and FRONTEND_SERVICE_SECRET.
  • Set FRONTEND_ORIGIN to the public HTTPS origin.
  • Set APLUS_URL to the browser-facing A+ origin.
  • Set DOCS_URL if the footer should link to a deployed copy of these docs.
  • Configure SMTP only if staff should email feedback from Hanami.
  • Configure LTI only after the A+ site administrator has created the matching service.
  • Leave every ALLOW_* and weak-secret setting unset.
  • Confirm that only ports 80 and 443 are public. Postgres and the direct frontend and backend ports should remain private.

Staff can email feedback documents to students. Delivery is via SMTP and is disabled until SMTP_HOST is set; see the environment variable reference for the full set of SMTP_* variables.

Postgres holds course configuration, rubrics, grading assignments, assessments, feedback documents, user preferences, template catalogs and extracted template files, manual-grading delivery state, and frontend session metadata. A+ tokens for active frontend sessions are encrypted before storage. Submission content, points, and student profiles are fetched from A+ and cached in process. Back up the Postgres volume; process-local caches are disposable.

For a portable backup, dump the database from the running container:

Terminal window
mkdir -p backups
docker compose exec -T db sh -c \
'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' | gzip > backups/hanami.sql.gz

The dump contains student data, authored feedback and grading data, encrypted A+ tokens, and the metadata needed to interpret those ciphertexts. Store it with the same access controls as the live database, protect the session encryption keys separately, and test the restore procedure before you need it.

docker compose down keeps named volumes. docker compose down -v deletes the database and must not be used on a deployment whose data you need.

See also the security model for what the deployment does and does not store about students.