Setup
DigitalOcean
Create the VM. A 2vCPU, 4GB RAM, 40GB disk instance is enough to get started.
Create a firewall rule to accept: Incoming SSH:22, HTTP:80, HTTPS:443; Outgoing ICMP, TCP:ALL, UDP:ALL
Ideally, create the VM in the same region as the image registry.
Cloudflare
Create A records for all public domains pointing as DNS-only to the VM IPv4.
SSH and Bootstrap
ssh root@<vm-ip>
git clone https://github.com/freeCodeCamp/exam-platform.git /opt/exam-platform
/opt/exam-platform/deploy/bootstrap.sh
NOTE: bootstrap.sh needs to be re-run any time there is a change to the Caddyfile.
Google Login (Optional)
Komodo reads login providers from Core’s environment only - the Komodo UI and API cannot set them. bootstrap.sh leaves them empty and disabled.
- Google Cloud Console: create an OAuth client (web application) with redirect URI
https://exam-platform.freecodecamp.dev/auth/google/callback. - On the Droplet, set in
/opt/exam-platform/deploy/.env:
KOMODO_GOOGLE_OAUTH_ENABLED=true
KOMODO_GOOGLE_OAUTH_ID=<client_id>
KOMODO_GOOGLE_OAUTH_SECRET=<client_secret>
- Restart Core
cd /opt/exam-platform/deploy
docker compose up -d core
NOTE: Re-running bootstrap.sh also applies it.
Admin still needs to create users, because sign-up is closed. Once a user logs in with password, they can link their Google account.
Examiner Dashboard OAuth
Create a separate Google OAuth web client and register https://examiner-dashboard.freecodecamp.dev/auth/google/callback. Login checks for a verified @freecodecamp.org Google Workspace account.
Generate cookie key with openssl rand -hex 64. Create these Komodo Variables before syncing/deploying the dashboard:
| Variable | Value |
|---|---|
EXAM_PLATFORM_EXAMINER_DASHBOARD_GOOGLE_OAUTH_ID | OAuth client ID |
EXAM_PLATFORM_EXAMINER_DASHBOARD_GOOGLE_OAUTH_SECRET | OAuth client secret; mark secret |
EXAM_PLATFORM_EXAMINER_DASHBOARD_GOOGLE_REDIRECT_URL | Registered callback URL above |
EXAM_PLATFORM_EXAMINER_DASHBOARD_COOKIE_KEY | Generated key; mark secret |
Examiner Database
Until production moves to hosted Postgres, eDb runs as Komodo stack exam-platform-examiner-db (databases/examiner/compose.yaml). It holds one schema per app plus shared, all owned by role edb_migrator; the apps connect as roles edd and aapi, which cannot change the schema. Generate four passwords with openssl rand -hex 32, then create secret Variables:
EXAM_PLATFORM_EXAMINER_DB_PASSWORD: superuserpostgres. Bootstrap only.EXAM_PLATFORM_EXAMINER_DB_MIGRATOR_PASSWORD,EXAM_PLATFORM_EXAMINER_DB_EDD_PASSWORD,EXAM_PLATFORM_EXAMINER_DB_AAPI_PASSWORD: rolesedb_migrator,eddandaapi.EXAM_PLATFORM_EXAMINER_DB_MIGRATOR_URL:postgres://edb_migrator:<migrator password>@examiner-db:5432/examiner. Both backend stacks pass it to theirmigrateservice only.EXAM_PLATFORM_EXAMINER_DASHBOARD_EDB_DATABASE_URL:postgres://edd:<edd password>@examiner-db:5432/examiner.EXAM_PLATFORM_AUTH_API_EDB_DATABASE_URL:postgres://aapi:<aapi password>@examiner-db:5432/examiner.
Deploy the eDb stack by hand before the first aAPI and eDd deploys. The deploy Action does not touch it. initdb/ creates the roles and schemas on an empty volume only: a volume from before edb_migrator existed has app-owned schemas and per-app migration tables, so delete it first. The deploy Action runs the image’s edb-migrate before every backend deploy, so aAPI and eDd deploy in any order. EXAM_PLATFORM_<APP>_MIGRATE_IMAGE Variables are created by the Action.
Komodo Log-In
- Open
https://exam-platform.freecodecamp.devand log in asadminwith the password from.env. Change the password, then add a passkey or TOTP. Nobody can sign up: admins add users under Settings → Users, and each user adds 2FA at first login. - Check that server
exam-platformreports OK. Its Periphery connects on start. - Settings → Providers: add an image registry account named
exam-platformforregistry.digitalocean.comwith a DigitalOcean token scoped to registry read. - Settings → Variables: create secret Variables
EXAM_PLATFORM_AUTH_API_SENTRY_DSN,EXAM_PLATFORM_EXAMINER_DASHBOARD_SENTRY_DSN. - Settings → Users: create service user
exam-platform-ciand an API key for it. Store the key in GitHub: Setup. - Syncs → new Resource Sync:
- Repository
freeCodeCamp/exam-platform, branchmain, resource pathdeploy/exam-platform.toml. - Include user groups on, include variables off, delete off.
- Execute it twice. The first run creates the Action after the user group, so the group’s Execute permission on it only applies on the second; until then, CI’s
RunActionfails with “User does not have required permissions on this Action”. Later edits to the file apply when an admin executes the sync again; it lists pending changes.
- Repository
The first deploy of each app creates its EXAM_PLATFORM_<APP>_IMAGE Variables. Until eDd’s first deploy, its name answers 502.
GitHub
Settings → Actions → General: allow GitHub Actions to create and approve pull requests for release-please.- Environment
staging: required reviewers, deployment branchmain.- Secrets
KOMODO_API_KEYandKOMODO_API_SECRET: the API key of Komodo service userexam-platform-ci. - Variable
KOMODO_URL:https://exam-platform.freecodecamp.dev.
- Secrets
- Repository secret
DIGITALOCEAN_USERNAME: DigitalOcean account email used for registry login. - Repository secret
DIGITALOCEAN_ACCESS_TOKEN: DigitalOcean token with registry read/write. - Repository variable
DOCR_REGISTRY: the registry name. - Branch protection: require
CI passed. Release PRs opened withGITHUB_TOKENtrigger no CI, so either allow merging them without the check, or give release-please a GitHub App token.
The secrets and variables, with the GitHub CLI:
gh secret set KOMODO_API_KEY --env staging
gh secret set KOMODO_API_SECRET --env staging
gh variable set KOMODO_URL --env staging --body https://exam-platform.freecodecamp.dev
gh secret set DIGITALOCEAN_USERNAME
gh secret set DIGITALOCEAN_ACCESS_TOKEN
gh variable set DOCR_REGISTRY --body <registry>