246 lines
8.9 KiB
Plaintext
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**.
|
|
|
|

|
|
|
|
## 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).
|
|
|
|
[](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.
|