Appearance
Install
This walks through bringing a fresh control plane up against an empty Kubernetes cluster, ending with your first hub running.
Don't have a licence yet?
Self-host is gated. Buy a licence and we'll email your private registry credentials immediately on payment.
1. Kubeconfig onto the control-plane host
The control plane talks to your tenant cluster via a kubeconfig file. Get one onto the host:
bash
# On your laptop:
doctl kubernetes cluster kubeconfig save <cluster-name>
# (or `gcloud container clusters get-credentials`, `aws eks update-kubeconfig`, etc.)
scp ~/.kube/config root@<host>:/opt/nucleus/kubeconfigbash
# On the host:
chmod 600 /opt/nucleus/kubeconfig2. Log into the registry and pull the bundle
The control-plane image and Helm chart live in our private OCI registry. Use the username + password from your purchase email.
bash
# Replace with the credentials from your purchase email
docker login registry.nucleuslms.io -u 'robot$nucleus+nuc-<your-id>'
# paste your one-time password
# Pull the control plane image
docker pull registry.nucleuslms.io/nucleus/control-plane:<version>
# Pull the Helm chart (OCI-format, no `helm repo add` needed)
helm pull oci://registry.nucleuslms.io/nucleus/nucleus-moodle --version <version>Replace <version> with the tag from our changelog, or use latest for the most recent release.
Lost your password?
The plaintext is shown once at issue and never stored in our database. If you've lost it, email support@nucleuslms.io and we'll rotate to a fresh credential pair.
3. Configure the control plane
The bundle ships an .env.production.example. Copy and fill it:
bash
cp .env.production.example .env.production
$EDITOR .env.productionRequired values:
| Variable | What it is |
|---|---|
POSTGRES_PASSWORD | Strong random — used for the control-plane Postgres |
BOOTSTRAP_ADMIN_EMAIL | First admin user, created on first boot |
BOOTSTRAP_ADMIN_PASSWORD | Initial password — you'll rotate it after first login |
ACME_EMAIL | Let's Encrypt registration email |
KUBECONFIG_HOST_PATH | Path to the kubeconfig you scp'd in step 1 |
NUCLEUS_ROOT_DOMAIN | The wildcard domain — e.g. your-domain |
| SMTP creds | SMTP_HOST, SMTP_USER, SMTP_PASS, SMTP_FROM |
4. Run the database migration
bash
docker run --rm --env-file .env.production \
registry.nucleuslms.io/nucleus/control-plane:<version> \
npx prisma migrate deploy5. Bring up the control plane
bash
docker compose -f deploy/compose/docker-compose.prod.yml --env-file .env.production up -dWatch the logs:
bash
docker compose logs -f control-planeYou're looking for nucleus-control-plane listening on :3000/api. Once that lands, your reverse proxy (Caddy by default) gets a TLS cert within ~30 seconds. Visit https://cp.your-domain and log in with the bootstrap admin credentials.
6. Smoke test
bash
curl -s https://cp.your-domain/api/healthz
# {"status":"ok"}
curl -s https://cp.your-domain/api/version
# {"api":"0.10.0","commit":"<sha>"}7. Provision your first hub
Self-Host doesn't ship the operator portal, so you'll do this via the API:
bash
TOKEN="<bootstrap-admin token>"
curl -X POST https://cp.your-domain/api/federations \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{
"slug": "first-hub",
"name": "First Hub",
"region": "lon1",
"mode": "content",
"tier": "Standard"
}'The hub provisions asynchronously — poll GET /api/federations/<id> or subscribe to the SSE stream at GET /api/federations/<id>/events to watch progress. Full endpoint list at API Reference.
8. Rotate the bootstrap password
There's no self-serve password change UI in the control plane API yet. Use the admin reset flow against your own user, copy the new plaintext out of the response (one-time reveal), and store it in your secret manager.