Skip to content

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/kubeconfig
bash
# On the host:
chmod 600 /opt/nucleus/kubeconfig

2. 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.production

Required values:

VariableWhat it is
POSTGRES_PASSWORDStrong random — used for the control-plane Postgres
BOOTSTRAP_ADMIN_EMAILFirst admin user, created on first boot
BOOTSTRAP_ADMIN_PASSWORDInitial password — you'll rotate it after first login
ACME_EMAILLet's Encrypt registration email
KUBECONFIG_HOST_PATHPath to the kubeconfig you scp'd in step 1
NUCLEUS_ROOT_DOMAINThe wildcard domain — e.g. your-domain
SMTP credsSMTP_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 deploy

5. Bring up the control plane

bash
docker compose -f deploy/compose/docker-compose.prod.yml --env-file .env.production up -d

Watch the logs:

bash
docker compose logs -f control-plane

You'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.

Released under the GPL v3 license.