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.
What ships in the repo
Section titled “What ships in the repo”- A production Dockerfile per app:
webapp,slides,pages,hook-station, andsite. 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:startis justdocker compose up -d.
There is no compose or Kubernetes file that wires all the services together for production. You bring your own.
The services in a full deployment
Section titled “The services in a full deployment”- 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.
Database
Section titled “Database”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.
Migrations
Section titled “Migrations”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
npx tsx packages/database/scripts/backfillRepoSubmissions.ts --applyto 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.
Building an image yourself
Section titled “Building an image yourself”Build any app from the repo root, passing the app’s Dockerfile:
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:
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 supported path today: Fly.io
Section titled “The supported path today: Fly.io”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:
npm run fly:webappnpm run fly:slidesnpm run fly:pagesnpm run fly:hooksnpm run fly:siteThe webapp’s release command runs database migrations on each deploy.
Configuration
Section titled “Configuration”Every service reads its config from environment variables. See Environment variables for the full list and which are required for which feature.