docs: selfhostin improvements

This commit is contained in:
Rotimi Best
2026-03-11 10:53:03 +01:00
parent d19dddd8cd
commit a98d6a35fa
11 changed files with 410 additions and 1226 deletions
@@ -47,9 +47,6 @@
$effect(() => {
console.log('content', content);
});
$effect(() => {
console.log('lessonApi.currentLocale', lessonApi.currentLocale);
});
</script>
{#if mode === MODES.edit}
@@ -22,7 +22,6 @@ ClassroomIO supports Single Sign-On (SSO) via OpenID Connect (OIDC), allowing yo
Set `LICENSE_KEY` in the API service environment.
Local development: `apps/api/.env`.
Docker Compose: repository root `.env`.
Published-image Docker deploys: `docker/.env.prod`.
If you do not use Enterprise-only features, leave `LICENSE_KEY` unset.
</Callout>
+186 -346
View File
@@ -4,288 +4,19 @@ description: Guide to self-hosting ClassroomIO on your own servers.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
This guide will help you quickly self-host ClassroomIO.
This guide covers two deployment paths: **Docker** (recommended) and **Railway**.
![Selfhost ClassroomIO](/selfhost-classroomio.webp)
ClassroomIO is made up of the following applications:
## Docker (Recommended)
- [apps/api](https://github.com/classroomio/classroomio/tree/main/apps/api) - an Express server
- [apps/dashboard](https://github.com/classroomio/classroomio/tree/main/apps/dashboard) - the admin dashboard + LMS
- [packages/db](https://github.com/classroomio/classroomio/tree/main/packages/db) - shared database layer for PostgreSQL
### Prerequisites
The quickest and easiest way to deploy both the api and dashboard is by using our [Railway Template](https://railway.app/template/dl9RG7?referralCode=8OyIj5). However there are a couple of things that you'd need first before heading over to the template, which are:
- Docker Desktop installed and running
- `git` installed
1. SMTP values - For sending emails on the api
2. PostgreSQL + Redis - For the API and database layer
3. Object storage (MinIO for Docker self-hosting, or Cloudflare R2 for hosted) - Video, document, and image uploads
## SMTP Setup
You need a custom SMTP configuration for email sending. Most importantly you need these values:
```zsh title="env"
SMTP_HOST=
SMTP_PORT=
SMTP_USER=
SMTP_PASSWORD=
SMTP_SENDER=
```
Make sure you have them handy because you'd need them while we go through this self-hosting guide.
<Callout title="Learn More">
Learn more about setting up a custom SMTP server
[here](https://nodemailer.com/smtp/)
</Callout>
## PostgreSQL + Redis
ClassroomIO now runs directly on PostgreSQL and Redis (no Supabase dependency in Docker setup).
For deployments, keep these connection variables ready:
```zsh title="env"
DATABASE_URL=
PRIVATE_DATABASE_URL=
REDIS_URL=
```
## Object Storage (Videos, Documents, Images)
ClassroomIO supports two object storage backends for video uploads, document uploads, and media (images/thumbnails):
### Option A: MinIO (S3-compatible, default for Docker self-hosting)
For Docker self-hosting, MinIO is included by default. Use MinIO or any S3-compatible storage for on-premise deployments:
```zsh title="env"
OBJECT_STORAGE_ENDPOINT=http://localhost:9000
OBJECT_STORAGE_PUBLIC_ENDPOINT=http://localhost:9000
OBJECT_STORAGE_ACCESS_KEY_ID=minioadmin
OBJECT_STORAGE_SECRET_ACCESS_KEY=minioadmin
OBJECT_STORAGE_FORCE_PATH_STYLE=true
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL=http://localhost:9000/media
```
**Docker:** MinIO is included by default when you run `./run-docker-full-stack.sh`. The script auto-configures these. Add to your `.env`:
```zsh title="env"
# MinIO container credentials (must match OBJECT_STORAGE_* below)
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
# When API runs in Docker: use minio (Docker service name), not localhost
OBJECT_STORAGE_ENDPOINT=http://minio:9000
OBJECT_STORAGE_PUBLIC_ENDPOINT=http://localhost:9000
OBJECT_STORAGE_ACCESS_KEY_ID=minioadmin
OBJECT_STORAGE_SECRET_ACCESS_KEY=minioadmin
OBJECT_STORAGE_FORCE_PATH_STYLE=true
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL=http://localhost:9000/media
```
<Callout type="warn" title="API in Docker">
When the API runs inside Docker, set `OBJECT_STORAGE_ENDPOINT=http://minio:9000`. From a container, `localhost` refers to the container itself; use the service name `minio` to reach MinIO on the Docker network. When the API runs locally (e.g. `pnpm api:dev`), use `OBJECT_STORAGE_ENDPOINT=http://localhost:9000`.
</Callout>
When you run `./run-docker-full-stack.sh`, MinIO starts automatically. To start MinIO manually (e.g. if you used `--no-minio`):
```zsh title="Terminal"
docker compose -f docker/docker-compose.yaml --profile minio up -d
```
**Access URLs:**
| Purpose | URL |
|---------|-----|
| API | http://localhost:9000 |
| Web UI | http://localhost:9001 (login: `minioadmin` / `minioadmin`) |
<Callout type="error" title="Change default credentials">
Do not use `minioadmin` / `minioadmin` in production. Set `MINIO_ROOT_USER` and `MINIO_ROOT_PASSWORD` in `.env`, and use the same values for `OBJECT_STORAGE_ACCESS_KEY_ID` and `OBJECT_STORAGE_SECRET_ACCESS_KEY`.
</Callout>
<Callout type="warn" title="MinIO Web UI not loading?">
MinIO uses a random port for the Web Console by default. If the Web UI at http://localhost:9001 does not load, remove the old container and recreate it: `docker rm -f cio-minio` then run the MinIO start command again. See [docker/docs/USAGE.md](https://github.com/classroomio/classroomio/blob/main/docker/docs/USAGE.md#minio-troubleshooting) for full troubleshooting.
</Callout>
<Callout title="Storage Optional">
With Docker, MinIO is included by default. Use `./run-docker-full-stack.sh --no-minio` to exclude it if you don't need video/document uploads.
</Callout>
### Option B: Cloudflare R2 (hosted deployments)
For hosted deployments (e.g. Railway), Cloudflare R2 can be used. Configure these env vars:
```zsh title="env"
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_ACCESS_KEY=
CLOUDFLARE_SECRET_ACCESS_KEY=
CLOUDFLARE_BUCKET_DOMAIN=
CLOUDFLARE_IMAGE_BUCKET_DOMAIN=
```
See the [Cloudflare Setup](/docs/contributor-guides/cloudflare-setup) guide for R2 bucket and CORS configuration.
## SSO Configuration
<Callout type="warning" title="Enterprise License Required">
SSO is an Enterprise-only feature for self-hosted instances.
If you do not use Enterprise-only features, you do not need a license key.
If you need Enterprise features, request a key from us and set `LICENSE_KEY` in your API environment.
[Book a call](http://cal.com/classroomio/enterprise) to get your license.
</Callout>
For Enterprise SSO support, you need to configure the following:
1. **License Key**: Set `LICENSE_KEY` in your API environment:
```zsh
LICENSE_KEY="your-issued-license-key"
```
2. **Trusted Origins**: Add your IdP domains to `TRUSTED_ORIGINS`:
```zsh
TRUSTED_ORIGINS="https://app.classroomio.com,https://your-org.okta.com,https://accounts.google.com"
```
3. **Callback URLs**: Ensure your `PUBLIC_SERVER_URL` is correctly set. SSO callbacks will be:
- `{PUBLIC_SERVER_URL}/api/auth/sso/callback/{providerId}`
4. **License Check**: The system will verify your Enterprise license before allowing SSO configuration. Without a valid license, the SSO settings page will show an upgrade prompt.
## Deploy on Railway
Now you have everything you need and you are ready to deploy the api and dashboard.
Go ahead and click this button to get started.
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/dl9RG7?referralCode=8OyIj5)
### Environment Variables
#### cio-api
```zsh title="env"
# Required
DATABASE_URL=""
REDIS_URL=""
PUBLIC_SERVER_URL=""
TRUSTED_ORIGINS=""
BETTER_AUTH_SECRET=""
AUTH_BEARER_TOKEN=""
AUTH_COOKIE_DOMAIN=""
PRIVATE_SERVER_KEY=""
SMTP_HOST=""
SMTP_PORT=""
SMTP_USER=""
SMTP_SENDER=""
SMTP_PASSWORD=""
# Optional - MinIO (S3-compatible, for Docker / on-premise self-hosting)
# When API runs in Docker: use minio service name. For local API: use http://localhost:9000
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
OBJECT_STORAGE_ENDPOINT=http://minio:9000
OBJECT_STORAGE_PUBLIC_ENDPOINT=http://localhost:9000
OBJECT_STORAGE_ACCESS_KEY_ID=minioadmin
OBJECT_STORAGE_SECRET_ACCESS_KEY=minioadmin
OBJECT_STORAGE_FORCE_PATH_STYLE=true
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL=http://localhost:9000/media
# Optional - Cloudflare R2 (for hosted deployments)
CLOUDFLARE_BUCKET_ID=""
CLOUDFLARE_ACCESS_KEY=""
CLOUDFLARE_ACCOUNT_ID=""
CLOUDFLARE_BUCKET_DOMAIN=""
CLOUDFLARE_SECRET_ACCESS_KEY=""
SENTRY_DNS=""
# Enterprise (Optional)
# Required only when using Enterprise-only features (SSO, token-auth, no-tracking)
LICENSE_KEY=""
# SSO (Optional)
# For enterprise SSO with SAML (future support)
# SSO_ENCRYPTION_KEY="your-32-character-secret-key"
```
<Callout title="Learn More">
Learn more about the environment variables
[here](https://github.com/classroomio/classroomio/blob/main/apps/api/.env.example)
</Callout>
#### cio-dashboard
```zsh title="env"
# Required
PUBLIC_SERVER_URL=""
PRIVATE_SERVER_KEY=""
PUBLIC_IS_SELFHOSTED="true"
# Required when self-hosting: CSP external domains (no defaults in self-hosted mode)
# Comma-separated list of domains allowed in Content-Security-Policy.
# Example: ALLOWED_EXTERNAL_DOMAINS=assets.cdn.clsrio.com,cdnjs.cloudflare.com,cdn.plyr.io,https://*.classroomio.com,https://api.classroomio.com,https://pgrest.classroomio.com,wss://*.classroomio.com
ALLOWED_EXTERNAL_DOMAINS=""
# Optional - per-directive override (script, style, connect, frame, font)
# CSP_SCRIPT_SRC_DOMAINS=""
# CSP_CONNECT_SRC_DOMAINS=""
# Optional
PRIVATE_SERVER_URL=""
OPENAI_API_KEY=""
UNSPLASH_API_KEY=""
PRIVATE_APP_HOST=""
PRIVATE_APP_SUBDOMAINS=""
# Optional - meta tags for custom branding (title, description, OG image)
# PUBLIC_APP_TITLE="My Learning Platform"
# PUBLIC_APP_DESCRIPTION="A flexible LMS for your organization"
# PUBLIC_OG_IMAGE_URL="https://app.yourdomain.com/og-image.png"
```
<Callout title="Important">
Set `PRIVATE_SERVER_KEY` in both API and dashboard environments for server-to-server API key authentication. `AUTH_BEARER_TOKEN` is a separate token and is not used by `apiKeyMiddleware`.
</Callout>
<Callout type="warn" title="CSP for Self-Hosted">
When `PUBLIC_IS_SELFHOSTED="true"`, the dashboard uses **only** environment variables for CSP external domains—no defaults. You must set `ALLOWED_EXTERNAL_DOMAINS` with a comma-separated list of domains your instance needs (CDN, analytics, video providers, etc.). This supports strict security policies, data residency, and CISO allowlists.
</Callout>
<Callout title="Important">
Dashboard URL routing is environment-aware:
browser requests use `PUBLIC_SERVER_URL`, while server-side dashboard requests use
`PRIVATE_SERVER_URL` (fallback: `PUBLIC_SERVER_URL`). In hosted deployments, set
`PUBLIC_SERVER_URL` to your public API domain (not `localhost`).
</Callout>
<Callout title="Important">
Set `AUTH_COOKIE_DOMAIN` to `.your-domain.com` for cross-subdomain cookies. If you need host-only cookies, set it
to your exact API host (for example `api.your-domain.com`).
</Callout>
<Callout title="Meta tags for self-hosted branding">
When self-hosting, page title, description, and Open Graph image default to your organization's branding (name, avatar, landing page banner). Set `PUBLIC_APP_TITLE`, `PUBLIC_APP_DESCRIPTION`, and `PUBLIC_OG_IMAGE_URL` to override with custom values. If unset, a bundled fallback image is used so the dashboard never depends on external CDNs.
</Callout>
<Callout title="Learn More">
Learn more about the environment variables
[here](https://github.com/classroomio/classroomio/blob/main/apps/dashboard/.env.example)
</Callout>
## Using Docker
We provide `docker/docker-compose.yaml` for local self-hosting.
The stack includes:
- `postgres` (database)
- `redis` (cache/rate-limit store)
- `db-init` (one-off DB setup)
- `api` (backend on `3081`)
- `dashboard` (frontend on `3082`)
### Quick Start
1. Clone the repository:
@@ -294,105 +25,214 @@ git clone https://github.com/classroomio/classroomio.git
cd classroomio
```
2. Start the full stack (includes MinIO for object storage by default):
2. Copy the env template and edit it:
```zsh title="Terminal"
cp .env.example .env
```
Open `.env` and set `PUBLIC_SERVER_URL`, `TRUSTED_ORIGINS`, `BETTER_AUTH_SECRET`, and `ALLOWED_EXTERNAL_DOMAINS` for your domain. For local development the defaults work as-is.
3. Start the full stack:
```zsh title="Terminal"
./run-docker-full-stack.sh
```
3. Verify services:
The script builds and starts Postgres, Redis, MinIO (object storage), the API, and the dashboard. It auto-generates secure values for `AUTH_BEARER_TOKEN` and `PRIVATE_SERVER_KEY` when they are missing.
4. Verify:
```zsh title="Terminal"
docker compose -p classroomio -f docker/docker-compose.yaml ps
curl -sS http://localhost:3081/
curl -I http://localhost:3082/
curl -sS http://localhost:3081/ # API — returns JSON
curl -I http://localhost:3082/ # Dashboard — returns 200
```
The services will be available at:
- Dashboard: http://localhost:3082
- API: http://localhost:3081
- MinIO Console: http://localhost:9001 (default login: `minioadmin` / `minioadmin`)
### API-Only Smoke Test
### One `.env`, One Command
If you want to validate API startup first:
Docker self-hosting uses a single root `.env` file. Every `docker compose` command reads it via `--env-file .env` — you never need separate env files per service. See [`.env.example`](https://github.com/classroomio/classroomio/blob/main/.env.example) for the full template.
### Environment Variables
#### Required
| Variable | Description | Local Default |
|----------|-------------|---------------|
| `POSTGRES_DB` | Database name | `classroomio` |
| `POSTGRES_USER` | Database user | `postgres` |
| `POSTGRES_PASSWORD` | Database password | `postgres` |
| `PUBLIC_SERVER_URL` | Public API URL (browser requests) | `http://localhost:3081` |
| `TRUSTED_ORIGINS` | Comma-separated CORS origins | `http://localhost:3082` |
| `BETTER_AUTH_SECRET` | Auth session signing secret | — |
| `AUTH_BEARER_TOKEN` | API bearer token | Auto-generated |
| `PRIVATE_SERVER_KEY` | Dashboard-to-API auth key (same in both) | Auto-generated |
| `PUBLIC_IS_SELFHOSTED` | Self-hosted mode flag | `true` |
| `PRIVATE_SERVER_URL` | Internal API URL for SSR (Docker network) | `http://api:3081` |
| `DASHBOARD_ORIGIN` | Dashboard URL for CSRF protection | `http://localhost:3082` |
| `ALLOWED_EXTERNAL_DOMAINS` | CSP allowed domains (comma-separated) | — |
<Callout type="warn" title="CSP Domains">
When `PUBLIC_IS_SELFHOSTED=true`, the dashboard has **no default** CSP domains. You must set `ALLOWED_EXTERNAL_DOMAINS` with your CDN, API, video, and font domains.
</Callout>
#### Optional — Email
Configure either SMTP or Zoho. Without email config, the app works but cannot send emails.
| Variable | Description |
|----------|-------------|
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_SENDER` | SMTP server |
| `ZOHO_TOKEN` | Zoho/ZeptoMail token (alternative to SMTP) |
#### Optional — Object Storage
MinIO is included by default. The startup script auto-configures these — you only need to change them if using custom credentials or an external S3-compatible service.
| Variable | Default |
|----------|---------|
| `MINIO_ROOT_USER` | `minioadmin` |
| `MINIO_ROOT_PASSWORD` | `minioadmin` |
| `OBJECT_STORAGE_ENDPOINT` | `http://minio:9000` |
| `OBJECT_STORAGE_PUBLIC_ENDPOINT` | `http://localhost:9000` |
| `OBJECT_STORAGE_ACCESS_KEY_ID` | `minioadmin` |
| `OBJECT_STORAGE_SECRET_ACCESS_KEY` | `minioadmin` |
| `OBJECT_STORAGE_FORCE_PATH_STYLE` | `true` |
| `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` | `http://localhost:9000/media` |
Use `./run-docker-full-stack.sh --no-minio` to exclude MinIO if you do not need uploads.
<Callout type="error" title="Change default credentials">
Do not use `minioadmin` / `minioadmin` in production.
</Callout>
#### Optional — Integrations
| Variable | Description |
|----------|-------------|
| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` | Google OAuth |
| `AUTH_COOKIE_DOMAIN` | Cross-subdomain cookies (e.g. `.yourdomain.com`) |
| `PRIVATE_APP_HOST`, `PRIVATE_APP_SUBDOMAINS` | Multi-subdomain routing |
| `UNSPLASH_API_KEY` | Unsplash image search |
| `LICENSE_KEY` | Enterprise features (SSO, token-auth, no-tracking) |
| `PUBLIC_APP_TITLE`, `PUBLIC_APP_DESCRIPTION`, `PUBLIC_OG_IMAGE_URL` | Custom branding |
### Common Commands
```zsh title="Terminal"
docker compose -p classroomio -f docker/docker-compose.yaml up --build -d postgres redis db-init api
docker compose -p classroomio -f docker/docker-compose.yaml logs --tail=100 api
curl -sS http://localhost:3081/
# Start full stack (builds images, starts all services including MinIO)
./run-docker-full-stack.sh
# Start without rebuilding images
./run-docker-full-stack.sh --no-build
# Start without MinIO (no file uploads)
./run-docker-full-stack.sh --no-minio
# Service status
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml ps
# Stream logs
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml logs -f api dashboard
# Rebuild one service
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml up -d --build api
# Restart after env change (no rebuild needed)
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml restart api dashboard
# Run DB seed
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml run --rm db-init sh -c "pnpm --filter @cio/db db:setup -- --seed"
# Stop
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml down
# Stop and delete all data (database, redis, minio volumes)
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml down -v
```
### Environment Variables for Docker
See [docker/docs/SELF_HOST.md](https://github.com/classroomio/classroomio/blob/main/docker/docs/SELF_HOST.md) for the full Docker reference including troubleshooting.
Local defaults are provided in compose, but set these for real deployments:
### Troubleshooting: "No space left on device"
```zsh title="Terminal"
docker system df # check usage
docker builder prune -f # free build cache
docker image prune -a -f # free unused images
docker container prune -f # free stopped containers
```
<Callout type="warn" title="Volume prune deletes data">
Avoid `docker volume prune` after `docker compose ... down` — it can remove your database and storage volumes.
</Callout>
Then restart: `./run-docker-full-stack.sh`
---
## Railway
The quickest hosted deployment is the [Railway Template](https://railway.app/template/dl9RG7?referralCode=8OyIj5).
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/dl9RG7?referralCode=8OyIj5)
### cio-api
```zsh title="env"
POSTGRES_DB=classroomio
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
DATABASE_URL="${{db.DATABASE_URL}}"
REDIS_URL="${{Redis.REDIS_URL}}"
PUBLIC_SERVER_URL="https://api.yourdomain.com"
TRUSTED_ORIGINS="https://api.yourdomain.com,https://app.yourdomain.com"
BETTER_AUTH_SECRET="random-secure-key"
AUTH_BEARER_TOKEN="some-key"
AUTH_COOKIE_DOMAIN="yourdomain.com"
PRIVATE_SERVER_KEY="same-key-for-api-and-dashboard"
BETTER_AUTH_SECRET=
AUTH_BEARER_TOKEN=
PRIVATE_SERVER_KEY=
# Required only for Enterprise-only features (SSO, token-auth, no-tracking)
LICENSE_KEY=
# Email (configure one)
SMTP_HOST=
SMTP_PORT=
SMTP_USER=
SMTP_PASSWORD=
SMTP_SENDER=
PUBLIC_SERVER_URL=
PRIVATE_SERVER_URL=
TRUSTED_ORIGINS=
AUTH_COOKIE_DOMAIN=
# MinIO (object storage, when using ./run-docker-full-stack.sh with MinIO)
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
OBJECT_STORAGE_ENDPOINT=http://minio:9000
OBJECT_STORAGE_PUBLIC_ENDPOINT=http://localhost:9000
OBJECT_STORAGE_ACCESS_KEY_ID=minioadmin
OBJECT_STORAGE_SECRET_ACCESS_KEY=minioadmin
OBJECT_STORAGE_FORCE_PATH_STYLE=true
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL=http://localhost:9000/media
# Object storage — Cloudflare R2 (for hosted deployments)
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_ACCESS_KEY=
CLOUDFLARE_SECRET_ACCESS_KEY=
CLOUDFLARE_BUCKET_DOMAIN=
CLOUDFLARE_IMAGE_BUCKET_DOMAIN=
```
<Callout title="Important">
`run-docker-full-stack.sh` auto-generates secure values for `AUTH_BEARER_TOKEN` and
`PRIVATE_SERVER_KEY` when missing/insecure. API key middleware for dashboard server-side
calls validates `PRIVATE_SERVER_KEY`. Set `LICENSE_KEY` only if you need Enterprise-only features; otherwise leave it empty.
Optional: add `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` for OAuth, `UNSPLASH_API_KEY` for image search, or `LICENSE_KEY` for enterprise features.
### cio-dashboard
```zsh title="env"
ALLOWED_EXTERNAL_DOMAINS="https://*.yourdomain.com,https://api.yourdomain.com,https://fonts.googleapis.com"
PRIVATE_APP_HOST="https://app.yourdomain.com"
PRIVATE_SERVER_KEY="same-key-for-api-and-dashboard"
PUBLIC_IS_SELFHOSTED="true"
PRIVATE_SERVER_URL="http://${{api.RAILWAY_PRIVATE_DOMAIN}}:8080"
PUBLIC_SERVER_URL="https://api.yourdomain.com"
```
Optional: add `PUBLIC_APP_TITLE`, `PUBLIC_APP_DESCRIPTION`, `PUBLIC_OG_IMAGE_URL` for custom branding.
<Callout title="PRIVATE_SERVER_KEY">
`PRIVATE_SERVER_KEY` must be the same value in both API and dashboard. It authenticates server-side dashboard requests to the API.
</Callout>
### If PostgreSQL or Docker fails with "No space left on device"
### SMTP Setup
The error `could not write lock file "postmaster.pid": No space left on device` means the disk (or Docker storage) is full. Free space, then restart the stack.
You need SMTP for email sending. See [Nodemailer SMTP docs](https://nodemailer.com/smtp/) for provider setup.
**1. Check disk usage**
### Cloudflare R2
```zsh title="Terminal"
df -h
docker system df
```
For hosted deployments, Cloudflare R2 handles object storage. See the [Cloudflare Setup](/docs/contributor-guides/cloudflare-setup) guide.
**2. Free Docker space** (safe — removes unused build cache and images)
### SSO (Enterprise)
```zsh title="Terminal"
docker builder prune -f
docker image prune -a -f
docker container prune -f
```
**3. If still low on space, remove unused volumes** (only if you do not need data in other projects’ volumes)
```zsh title="Terminal"
docker volume prune -f
```
<Callout type="warn" title="Volume prune can delete your data">
`docker volume prune` removes volumes not used by any container. If you run it after `docker compose ... down`, it can remove Classroom.io’s `postgres-data`, `redis-data`, and `minio-data`, and you will lose database and object storage data. To free space without touching Classroom.io data, use only the prunes in step 2, or remove specific unused volumes by name.
</Callout>
**4. Restart the stack**
```zsh title="Terminal"
./run-docker-full-stack.sh
```
If the host disk is still full after pruning, free space elsewhere (e.g. large files, old logs, other apps) or increase disk capacity. See [docker/docs/SELF_HOST.md](https://github.com/classroomio/classroomio/blob/main/docker/docs/SELF_HOST.md) for the full self-host runbook.
SSO requires a license key. Set `LICENSE_KEY` in your API environment and add your IdP domains to `TRUSTED_ORIGINS`. [Book a call](http://cal.com/classroomio/enterprise) to get a license.