Skip to content

Self-hosting with Docker

How to self-host Classmoji with Docker

Classmoji ships a production Dockerfile for every app, so you can build and run it yourself. What it does not ship yet is a single turnkey docker compose up that brings up the whole stack. You either run each image with your own orchestration, or use the Fly.io setup the project already includes. This page is honest about both.

  • A production Dockerfile per app: webapp, slides, pages, hook-station, and site. Each builds on Node 24 Alpine, generates the Prisma client at build time, and builds the app.
  • A root docker-compose.yml, for local development only. It runs Postgres only. It does not run the webapp, slides, pages, hook-station, or site. npm run db:start is just docker compose up -d.

There is no compose or Kubernetes file that wires all the services together for production. You bring your own.

  • Postgres: the only datastore
  • webapp: the main app
  • slides and pages: companion apps for presentations and content
  • hook-station: receives Github and Stripe webhooks
  • site: this marketing and docs site (optional)
  • background jobs (Trigger.dev, packages/tasks): also run quizzes and Ask Moji as chat agents. Those two agents live in a private module and may be unavailable to you; without it, a background-jobs deploy refuses to build, so quizzes and Ask Moji cannot run.

Postgres is the only database. For local work, npm run db:start brings up the dev Postgres defined in docker-compose.yml. For a real deployment, we recommend Neon, the managed Postgres the hosted version of Classmoji runs on. Create a Neon project, copy its connection string, and set it as DATABASE_URL.

Each image generates the Prisma client at build time, but you still need to apply migrations to your database. Run prisma migrate deploy against DATABASE_URL before serving traffic. In the hosted setup, the webapp runs migrate deploy automatically as a release step on every deploy, so you don’t have to do it by hand there.

Upgrading to the coursework model (assignments in modules, push-mode submissions). Four migrations carry existing data over: 20260920000000_coursework_phase1_additive, 20260920000001_coursework_phase1_constraints, 20260920000002_submission_modes, and 20260920000003_one_score_per_grader. They run with prisma migrate deploy like any other. Afterwards, run

Terminal window
npx tsx packages/database/scripts/backfillRepoSubmissions.ts --apply

to stamp push-mode submissions from each student repo’s push history (run it without --apply first for a dry run). The migrations record what they changed in the coursework_migration_report table, so you can review moved weights and rewritten records before opening the app to students.

Build any app from the repo root, passing the app’s Dockerfile:

Terminal window
docker build -f apps/webapp/Dockerfile -t classmoji-webapp .

If a proxy in front of the webapp or slides terminates TLS (most reverse proxies and load balancers do), build those two images with the public host each one is served from, or their forms won’t submit:

Terminal window
docker build -f apps/webapp/Dockerfile --build-arg ACTION_ORIGIN_HOSTS=classmoji.example.edu -t classmoji-webapp .
docker build -f apps/slides/Dockerfile --build-arg ACTION_ORIGIN_HOSTS=slides.classmoji.example.edu -t classmoji-slides .

ACTION_ORIGIN_HOSTS takes exact host names, comma-separated, with no scheme, port or wildcard. It is read when the image is built, not when the container starts, so a new host means a new build.

Then run it with the environment variables that app needs. Because there’s no bundled orchestration, you’re responsible for networking the services together, supplying secrets, and putting a reverse proxy in front. Compose, Kubernetes, or a PaaS all work.

The project includes a fly.toml and a deploy script for each app, and this is the most worn path. With the Fly CLI installed and authenticated:

Terminal window
npm run fly:webapp
npm run fly:slides
npm run fly:pages
npm run fly:hooks
npm run fly:site

The webapp’s release command runs database migrations on each deploy.

Every service reads its config from environment variables. See Environment variables for the full list and which are required for which feature.