Skip to content

H5P service in production

H5P content is served by its own Node service (api/h5p, GPL, reached over HTTP only; see Licensing). It is multi-tenant: the request host selects the tenant’s database, bucket and Passport public key. This page covers running it in production; the service itself is described in api/h5p/README.md.

The development stack mounts the whole api/ folder into the service, which also exposes the application code, every secret of every env file and the Passport private keys. In production the service reads a config directory that holds, per tenant, only the database, bucket and URL settings it needs and the Passport public key. The API writes it (H5PServiceConfigExporter):

<dir>/.env platform subset
<dir>/.env.<host> tenant subset
<dir>/keys/oauth-public.key and <dir>/keys/<host_with_underscores>/oauth-public.key
  1. Set the directory on the API, in the platform environment (the production example does this with a dedicated volume):

    LARAVEL_H5P_SERVICE_CONFIG_DIR=/var/www/html/storage/h5p-service

    That becomes H5P_SERVICE_CONFIG_DIR in the platform .env. Empty means “not exported” (development).

  2. Start the API, then export once:

    Terminal window
    docker compose exec api php artisan ulams:h5p:export-config

    The export also runs at start (ulams:tenant:sync-env) and whenever a tenant is created, synced or deleted. ulams:upgrade runs it as a step too, so after every release the directory matches the tenants table. Run it by hand after creating tenants outside those commands or after restoring a backup.

  3. Mount the directory read-only into the service with the production override (api/h5p/compose.h5p.prod.yml; the example docker-compose.yml already contains the same settings):

    Terminal window
    docker compose -f docker-compose.yml -f h5p/compose.h5p.prod.yml up -d h5p

    It sets ENV_DIR=/config, KEYS_DIR=/config/keys and JWT_PUBLIC_KEY_PATH=/config/keys/oauth-public.key, mounts ${H5P_SERVICE_CONFIG_HOST_DIR:-./storage/h5p-service} at /config:ro, makes the container’s root file system read-only and gives it a tmpfs for /tmp. The override uses !override (Docker Compose 2.24 or newer).

The service re-reads the files every TENANT_RELOAD_CHECK_MS, so a new tenant works without a restart. The files are written atomically and, when the command runs as root, handed to the owner and group of the Laravel storage directory; the service reads them through the same group (group_add: ["82"] in the example).

A tenant with no H5P request for 30 minutes (TENANT_IDLE_EVICT_MS) releases its database pool and S3 client; its next request reconnects with a short delay and no visible error. Set it to 0 to keep every tenant connected.

Installing, updating or deleting H5P libraries changes them for all tenants, so the service accepts it only on the platform host (the platform admin, see H5P libraries). The h5p_libraries volume holds them; back it up with the other volumes (Backups).

Access tokens live about 5 minutes. The admin and the React front refresh them in the background and hand every new token to the H5P frame, which keeps saving the learner’s state without reloading the content.

The reference front (web) keeps the learner’s token in an httpOnly session cookie, so it never hands a token to the H5P frame (the embed page is told token: null). H5P content is third-party script, and a token inside the frame could be read by it. Instead the front serves the H5P service on its own origin at /h5p/* (a same-origin proxy to <tenant API>/h5p/*) and adds the session token on the server, only for the learner’s own player calls (ADR 0045):

Method and path (after /h5p/) Purpose
GET embed/play/<id> The embed page
GET contents/<id>/play The play model
GET, POST contentUserData/<id>/<type>/<sub> Load and save the learner’s state
POST finishedData Save the result

Every other route and method (libraries, editor, content management) is forwarded without the session, and a _token query parameter sent by the caller is dropped. The proxy marks its requests with X-Ulams-Session-Proxy: 1 so the H5P service leaves the learner’s token out of the URLs it puts in the play model; the frame’s calls then come back through the proxy. Learners’ answers count for progress and the content restores where they left off.

For operators this changes nothing to configure: H5P traffic of logged-in learners passes through the web container, which must be able to reach the tenant API host (it already must for everything else), and the proxy adds a little latency.

H5P runs on the application origin, not on the content origin (it calls its service with fetch), so a separate content domain does not change anything here; see Content origin.