Files
classroomio/apps/docs/content/docs/quickstart/self-hosting.mdx
T

246 lines
8.9 KiB
Plaintext

---
title: Self-hosting ClassroomIO
description: Guide to self-hosting ClassroomIO on your own servers.
---
import { Callout } from 'fumadocs-ui/components/callout';
This guide covers two deployment paths: **Docker** (recommended) and **Railway**.
![Selfhost ClassroomIO](/selfhost-classroomio.webp)
## Docker (Recommended)
### Prerequisites
- Docker Desktop installed and running
- `git` installed
### Quick Start
1. Clone the repository:
```zsh title="Terminal"
git clone https://github.com/classroomio/classroomio.git
cd classroomio
```
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
```
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"
curl -sS http://localhost:3081/ # API — returns JSON
curl -I http://localhost:3082/ # Dashboard — returns 200
```
- Dashboard: http://localhost:3082
- API: http://localhost:3081
- MinIO Console: http://localhost:9001 (default login: `minioadmin` / `minioadmin`)
### One `.env`, One Command
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` | Root domain for 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"
# 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
```
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.
### 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"
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"
PRIVATE_SERVER_KEY="same-key-for-api-and-dashboard"
PUBLIC_IS_SELFHOSTED="true"
# Required for auth cookies when API and dashboard use different subdomains — set to your root domain, e.g. yourdomain.com
AUTH_COOKIE_DOMAIN="yourdomain.com"
# Email (configure one)
SMTP_HOST=
SMTP_PORT=
SMTP_USER=
SMTP_PASSWORD=
SMTP_SENDER=
# 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 type="warn" title="AUTH_COOKIE_DOMAIN (Railway)">
When API and dashboard use different subdomains, set `AUTH_COOKIE_DOMAIN` to your root domain (e.g. `yourdomain.com`). Without it, Better Auth cookies will not be set and login will fail.
</Callout>
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>
### SMTP Setup
You need SMTP for email sending. See [Nodemailer SMTP docs](https://nodemailer.com/smtp/) for provider setup.
### Cloudflare R2
For hosted deployments, Cloudflare R2 handles object storage. See the [Cloudflare Setup](/docs/contributor-guides/cloudflare-setup) guide.
### SSO (Enterprise)
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.