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.
Quick start
Section titled “Quick start”cp .env.example .env# Set POSTGRES_PASSWORD, FRONTEND_SESSION_ENCRYPTION_KEY, and# FRONTEND_SERVICE_SECRET in .env.docker compose up --buildOpen 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:
docker compose psdocker compose logs --tail=100 frontend backend dbPOSTGRES_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.
Pointing at your A+ instance
Section titled “Pointing at your A+ instance”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.
Production HTTPS with Caddy
Section titled “Production HTTPS with Caddy”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.comFRONTEND_ORIGIN=https://hanami.example.comPROTOCOL_HEADER=x-forwarded-protoHOST_HEADER=x-forwarded-hostADDRESS_HEADER=x-forwarded-forXFF_DEPTH=1Start the stack with the edge profile:
docker compose --profile edge up -d --buildThe 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:
curl --http2 -I https://hanami.example.comcurl --http3-only -I https://hanami.example.comThe 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.
Before opening a production deployment
Section titled “Before opening a production deployment”- Set a unique
POSTGRES_PASSWORDand keep.envreadable only by the deployment account. - Generate independent values for
FRONTEND_SESSION_ENCRYPTION_KEYandFRONTEND_SERVICE_SECRET. - Set
FRONTEND_ORIGINto the public HTTPS origin. - Set
APLUS_URLto the browser-facing A+ origin. - Set
DOCS_URLif 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.
Feedback email
Section titled “Feedback email”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.
What state lives where
Section titled “What state lives where”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:
mkdir -p backupsdocker compose exec -T db sh -c \ 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' | gzip > backups/hanami.sql.gzThe 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.