refactor: selfhosting docs (#798)f
* Remove the run-docker-full-stack.sh script to streamline the deployment process and eliminate unnecessary complexity. * fix: update documentation to clarify script behavior for building and pulling images * update docs * fix: update script URL to use classroomio.com for downloads * fix: humanize selfhosting docs and minor amendment * fix: attend to comment * fix: humanize docs * update docs * fix: adjusted the Restarting ClassroomIO... function * fix: update commands and documentation for environment variable handling and upgrades * chore: added space for rebuild
This commit is contained in:
+3
-3
@@ -26,7 +26,7 @@ POSTGRES_PASSWORD=postgres
|
|||||||
# Auth secret for session signing. REQUIRED — the stack refuses to start if this is empty.
|
# Auth secret for session signing. REQUIRED — the stack refuses to start if this is empty.
|
||||||
BETTER_AUTH_SECRET=
|
BETTER_AUTH_SECRET=
|
||||||
|
|
||||||
# Dashboard-to-API auth key (leave blank — auto-generated by ./run-docker-full-stack.sh).
|
# Dashboard-to-API auth key (leave blank — auto-generated by ./classroomio.sh).
|
||||||
# Must be the same value in both API and dashboard.
|
# Must be the same value in both API and dashboard.
|
||||||
PRIVATE_SERVER_KEY=
|
PRIVATE_SERVER_KEY=
|
||||||
|
|
||||||
@@ -82,10 +82,10 @@ SMTP_PASSWORD=
|
|||||||
SMTP_SENDER=
|
SMTP_SENDER=
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# Optional — Object Storage (auto-configured by ./run-docker-full-stack.sh)
|
# Optional — Object Storage (auto-configured by ./classroomio.sh)
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
# MinIO is included by default. On first run ./run-docker-full-stack.sh RANDOMIZES the
|
# MinIO is included by default. On first run ./classroomio.sh RANDOMIZES the
|
||||||
# MinIO credentials below and mirrors them into OBJECT_STORAGE_ACCESS_KEY_ID/SECRET.
|
# MinIO credentials below and mirrors them into OBJECT_STORAGE_ACCESS_KEY_ID/SECRET.
|
||||||
# These minioadmin placeholders are only used if you skip the script.
|
# These minioadmin placeholders are only used if you skip the script.
|
||||||
MINIO_ROOT_USER=minioadmin
|
MINIO_ROOT_USER=minioadmin
|
||||||
|
|||||||
@@ -1,636 +0,0 @@
|
|||||||
# ClassroomIO — Feature & Flow Map (QA Audit)
|
|
||||||
|
|
||||||
> **Purpose of this document:** a reverse‑engineered, citation‑backed map of every
|
|
||||||
> feature in the ClassroomIO application, produced for QA to build test cases and
|
|
||||||
> record demo videos. **Files are the source of truth.** Every claim is backed by a
|
|
||||||
> file reference. Claims that could not be traced to code are marked
|
|
||||||
> `⚠️ UNVERIFIED`. Ambiguities live in **§7 Open Questions**, not in the body.
|
|
||||||
>
|
|
||||||
> Reference format: `` `path/to/file` `` → symbol/lines — what it proves.
|
|
||||||
> Scope (per QA request): full depth on the user‑facing product
|
|
||||||
> (`apps/dashboard`, `apps/api`, `apps/website`, `apps/embeds` + the packages that
|
|
||||||
> back features); a lighter **Supporting Infrastructure** summary for the rest.
|
|
||||||
> PRD folders (`prd/*`) are used only to cross‑check intent — code wins on conflict.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. System Overview
|
|
||||||
|
|
||||||
**What it is.** ClassroomIO is an open‑source **Learning Management System (LMS) for
|
|
||||||
companies** — compliance/employee training, customer education, and partner
|
|
||||||
certification. It supports multi‑organization workspaces, courses/lessons/exercises,
|
|
||||||
cohorts (cohorts), certificates, an AI course builder + in‑lesson tutor, a public
|
|
||||||
REST API, an MCP server, and embeddable course widgets. It can be **self‑hosted** or
|
|
||||||
run as the **cloud** product. (`README.md` → lines 11–50 — feature summary;
|
|
||||||
`README.md` → 84–91 — monorepo apps.)
|
|
||||||
|
|
||||||
**Tech stack** (cited from manifests):
|
|
||||||
- **Monorepo**: pnpm + Turbo. `package.json` → root scripts/workspaces;
|
|
||||||
`pnpm-workspace.yaml` → packages globs (`apps/*`, `packages/*`,
|
|
||||||
`packages/course-app/src/*`); `turbo.json`.
|
|
||||||
- **Frontend**: SvelteKit + Svelte 5 + TailwindCSS. `README.md` → 52–58 ("Built With");
|
|
||||||
`apps/dashboard/svelte.config.js`.
|
|
||||||
- **Backend API**: Hono (TypeScript). `apps/api/src/app.ts` → `new Hono()` app;
|
|
||||||
`ARCHITECHTURE.md` → layered architecture (Routes → Services → Queries).
|
|
||||||
- **Database**: PostgreSQL via Drizzle ORM. `packages/db/src/schema.ts` (~3,400 lines
|
|
||||||
of `pgTable`/`pgEnum`); `packages/db/src/drizzle.ts`.
|
|
||||||
- **Auth**: Better Auth (sessions, OAuth, SSO). `apps/api/src/app.ts` → 12, 107–188
|
|
||||||
(`auth.handler`, `/api/auth/*`); `packages/db/src/auth.ts`.
|
|
||||||
- **Background jobs**: BullMQ + Redis. `apps/jobs`, `apps/api/src/routes/admin/queues.ts`
|
|
||||||
(`mountQueueDashboard`), `mediaJob`/`jobStep`/`deadLetterJob` tables in `schema.ts`.
|
|
||||||
- **Object storage**: S3‑compatible / MinIO. `.env.example` → 77–89 (OBJECT_STORAGE_*);
|
|
||||||
`README.md` → 176–191.
|
|
||||||
- **Billing**: Polar. `apps/dashboard/src/routes/api/polar/*`.
|
|
||||||
- **AI providers**: OpenAI / Google (Gemini) / Anthropic. `.env.example` → 110–115;
|
|
||||||
`packages/utils/src/agent-models` (per `README.md` → 236).
|
|
||||||
|
|
||||||
**High‑level architecture** (`ARCHITECHTURE.md` → 9–51):
|
|
||||||
```
|
|
||||||
SvelteKit dashboard (apps/dashboard) ──RPC (Hono client)──▶ Hono API (apps/api)
|
|
||||||
│ server-side SSR proxy (.server.ts, PRIVATE_SERVER_KEY) │
|
|
||||||
│ ▼
|
|
||||||
Public org sites / embeds Services → Queries (Drizzle)
|
|
||||||
(apps/website, apps/embeds) │
|
|
||||||
▼
|
|
||||||
PostgreSQL · Redis/BullMQ · S3/MinIO
|
|
||||||
```
|
|
||||||
- Layered API: **Routes** (HTTP/validation/auth) → **Services** (business logic/
|
|
||||||
transactions) → **Queries** (pure Drizzle). `ARCHITECHTURE.md` → 44–50.
|
|
||||||
- Shared **Zod validation** in `packages/utils/src/validation/*`, used by both API and
|
|
||||||
dashboard. `ARCHITECHTURE.md` → 45; `AGENTS.md` → 17–25.
|
|
||||||
- Dashboard talks to the API through a typed Hono RPC client and an SSR auth proxy
|
|
||||||
authenticated with `PRIVATE_SERVER_KEY`. `README.md` → 149–156.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Feature Inventory
|
|
||||||
|
|
||||||
Status legend: **Complete** = route+service+schema present; **Partial** = present but
|
|
||||||
gated/limited or doc claims exceed code; **Stub/Planned** = PRD only, little/no code.
|
|
||||||
|
|
||||||
| # | Feature | Purpose | Primary files | Status |
|
|
||||||
|---|---------|---------|---------------|--------|
|
|
||||||
| 1 | Authentication & sessions | Email/password, OAuth, session mgmt | `apps/api/src/app.ts:107`, `packages/db/src/auth.ts` | Complete |
|
|
||||||
| 2 | Signup gating | Org‑scoped/invite‑only/self‑host bootstrap rules | `apps/api/src/middlewares/signup-guard.ts` | Complete |
|
|
||||||
| 3 | Onboarding & org creation | Create profile + first organization | `apps/api/src/services/onboarding.ts`, `apps/dashboard/src/routes/(auth)/onboarding` | Complete |
|
|
||||||
| 4 | Organizations (workspaces) | Multi‑org, settings, members, plan | `apps/api/src/routes/organization/organization.ts`, `schema.ts:1949` | Complete |
|
|
||||||
| 5 | Team / people management | Invite teachers, assign roles | `apps/api/src/routes/course/people.ts`, `apps/dashboard/src/lib/features/people` | Complete |
|
|
||||||
| 6 | Courses | Create/manage courses, sections, content | `apps/api/src/routes/course/course.ts`, `schema.ts:634` | Complete |
|
|
||||||
| 7 | Lessons & lesson editor | Lesson content, multilingual, comments | `apps/api/src/routes/course/lesson.ts`, `.../lesson-language.ts`, `schema.ts:956` | Complete |
|
|
||||||
| 8 | Exercises & questions | Quizzes, question types, sections, grading | `apps/api/src/routes/course/exercise.ts`, `.../submission.ts`, `.../mark.ts`, `packages/question-types` | Complete |
|
|
||||||
| 9 | Course people / enrollment | Enroll students, roster | `apps/api/src/routes/course/people.ts` | Complete |
|
|
||||||
| 10 | Course invites | Email + link invites with audit | `apps/api/src/routes/course/invite.ts`, `apps/api/src/services/course/invite.ts`, `schema.ts:1282` | Complete |
|
|
||||||
| 11 | Org invites | Invite org members (EMAIL/LINK) | `apps/api/src/routes/invite/invite.ts`, `schema.ts:1384` | Complete |
|
|
||||||
| 12 | Compliance training | Deadlines, renewals, grace, records | `apps/api/src/routes/course/compliance.ts`, `apps/api/src/routes/internal`, `schema.ts:768` (`courseCompletionRecord`) | Complete |
|
|
||||||
| 13 | Certificates | Issue branded certificates | `packages/certificates/src/render.ts`, `schema.ts:850` (`courseCertificateIssue`) | Complete |
|
|
||||||
| 14 | Cohorts | Group courses, goals, members, newsfeed | `apps/api/src/routes/cohort/cohort.ts`, `schema.ts` cohort tables | Complete |
|
|
||||||
| 15 | Community Q&A | Per‑course questions/answers | `apps/api/src/routes/community/community.ts`, `schema.ts:1558` | Complete |
|
|
||||||
| 16 | Course newsfeed | Course/cohort announcements + comments | `apps/api/src/routes/course/newsfeed.ts`, `schema.ts:1605` | Complete |
|
|
||||||
| 17 | Attendance | Track group/live‑class attendance | `apps/api/src/routes/course/attendance.ts`, `schema.ts:321` (`groupAttendance`) | Complete |
|
|
||||||
| 18 | AI Course Assistant | In‑course AI authoring/plan/edit chat | `apps/api/src/routes/agent/*`, `packages/ai-assistant`, `schema.ts:3163` (`aiAgentRun`) | Complete (key‑gated) |
|
|
||||||
| 19 | AI Lesson Tutor | In‑lesson learner AI helper + fair‑use caps | `apps/api/src/routes/course/ai-tutor.ts`, `apps/api/src/routes/organization/ai-tutor.ts`, `schema.ts:3013` (`aiTutorMessageCount`) | Complete (key‑gated) |
|
|
||||||
| 20 | AI credits / token billing | Token usage, balance, purchases | `schema.ts:2919` (`aiTokenUsage`), `apps/dashboard/src/lib/features/agent/api/credit-purchase.server.ts` | Complete (cloud billing) |
|
|
||||||
| 21 | Course import | Import from prompt/PDF/course | `apps/api/src/routes/organization/course-import.ts`, `apps/api/src/services/course-import`, `schema.ts:2603` | Complete |
|
|
||||||
| 22 | Media manager | Upload/manage video/docs/media, transcripts | `apps/api/src/routes/media/media.ts`, `.../transcripts.ts`, `.../hls.ts`, `apps/dashboard/src/lib/features/media`, `schema.ts:1028` (`asset`) | Complete |
|
|
||||||
| 23 | Tags | Tag groups/tags/assignments for courses | `apps/api/src/routes/organization/tags.ts`, `schema.ts:2158` | Complete |
|
|
||||||
| 24 | Org public site (org‑site) | Branded public course catalog/pages | `apps/api/src/routes/org-site`, `apps/dashboard/src/routes/(org-site)` | Complete |
|
|
||||||
| 25 | Custom domains | Cloudflare custom domain mapping | `apps/api/src/routes/domain/domain.ts`, `apps/tenant-router` | Complete |
|
|
||||||
| 26 | Course widget embed | Embeddable course catalog widget | `apps/api/src/routes/widgets`, `apps/dashboard/src/routes/(app)/widgets`, `apps/embeds`, `schema.ts:2255` (`widget`) | Complete |
|
|
||||||
| 27 | Public REST API (v1) | API‑key automation: courses, audience | `apps/api/src/routes/v1/*`, `apps/api/src/services/v1`, `schema.ts:2539` (`organizationApiKey`) | Complete |
|
|
||||||
| 28 | Automation / API keys | Issue mcp/api/zapier keys with scopes | `apps/api/src/routes/organization/automation.ts`, `apps/api/src/middlewares/automation-key*.ts` | Complete |
|
|
||||||
| 29 | MCP server | `@classroomio/mcp` AI‑native course authoring | `packages/mcp/src/*` | Complete |
|
|
||||||
| 30 | Enterprise SSO | OIDC SSO (Okta/Google Workspace/Auth0) | `apps/api/src/routes/organization/sso.ts`, `apps/api/src/routes/sso/discovery.ts`, `schema.ts:2479` (`organizationSsoConfig`) | Complete (licensed) |
|
|
||||||
| 31 | Token auth | Token‑based auth policy | `apps/api/src/routes/organization/token-auth.ts`, `schema.ts:2513` (`organizationTokenAuth`) | Complete (licensed) |
|
|
||||||
| 32 | Licensing | Self‑host license‑gated features | `apps/api/src/routes/license.ts`, `apps/api/src/middlewares/license.ts`, `packages/utils/src/license/*` | Complete |
|
|
||||||
| 33 | Billing (Polar) | Subscriptions + AI token packs | `apps/dashboard/src/routes/api/polar/*` | Complete (cloud only) |
|
|
||||||
| 34 | Analytics | Login/page events, org/course/country daily | `apps/api/src/routes/dash`, `apps/api/src/services/analytics`, `schema.ts:167–285` | Complete |
|
|
||||||
| 35 | Email / notifications | Transactional emails via jobs | `apps/api/src/routes/mail`, `packages/email`, `apps/api/src/services/jobs` | Complete |
|
|
||||||
| 36 | Background jobs | Media/email queues, BullMQ dashboard | `apps/jobs`, `apps/api/src/routes/jobs`, `apps/api/src/routes/admin/queues.ts` | Complete |
|
|
||||||
| 37 | Unsplash image search | Image picker for content | `apps/api/src/routes/unsplash/unsplash.ts` | Complete |
|
|
||||||
| 38 | Marketing website | classroomio.com landing | `apps/website` | Complete |
|
|
||||||
| 39 | Documentation site | classroomio.com/docs | `apps/docs` | Complete |
|
|
||||||
| 40 | Outbound webhooks | Emit `certificate.issued`, etc. | `prd/webhooks` (todo) | **Stub/Planned ⚠️** |
|
|
||||||
| 41 | SCORM support | SCORM package support | `prd/scorm-support` (todo) | **Stub/Planned ⚠️** |
|
|
||||||
| 42 | Notification system | In‑app notifications | `prd/notification-system` (todo) | **Stub/Planned ⚠️** |
|
|
||||||
|
|
||||||
> Items 40–42 are listed in `PRD-TRACKER.md` → 9, 16, 24 as **todo**; treat as not yet
|
|
||||||
> shipped. See **§7**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Per‑Feature Detail
|
|
||||||
|
|
||||||
### 3.1 Authentication & Sessions
|
|
||||||
- **Purpose:** authenticate users (email/password + OAuth) and attach org roles to the session.
|
|
||||||
- **How it works:** All `/api/auth/*` requests are handled by Better Auth via
|
|
||||||
`auth.handler`. `apps/api/src/app.ts` → 107–188. A global middleware resolves the
|
|
||||||
session on every request and sets `user`, `session`, and `orgRoles` into context.
|
|
||||||
`apps/api/src/app.ts` → 60–97. The app rebuilds the request host from
|
|
||||||
`X-Forwarded-Host`/`-Proto` (Cloudflare proxy) and uses a Redis "oauth handoff" to
|
|
||||||
shrink large OAuth callback cookies into a short token. `apps/api/src/app.ts` → 110–173.
|
|
||||||
- **User flow:** Login page → submit email/password (or OAuth) → Better Auth sets
|
|
||||||
session cookie → dashboard reads `/session`. `apps/dashboard/src/routes/(auth)/login`;
|
|
||||||
`apps/api/src/app.ts` → 189–199 (`/session`).
|
|
||||||
- **Key files:** `apps/api/src/app.ts`, `packages/db/src/auth.ts`,
|
|
||||||
`packages/db/src/auth/hooks/create-profile.ts`.
|
|
||||||
- **Edge cases:** OAuth cookie > 500 chars routed through Redis token (`app.ts:160`);
|
|
||||||
`getSession` failures fall through as anonymous (`app.ts:71–84`); session carries
|
|
||||||
`orgRoles` map used by RBAC middlewares (`app.ts:88`).
|
|
||||||
|
|
||||||
### 3.2 Signup Gating (`signupGuard`)
|
|
||||||
- **Purpose:** enforce org‑level and deployment‑level signup rules server‑side.
|
|
||||||
- **How it works:** `apps/api/src/middlewares/signup-guard.ts` → 129–159. With a
|
|
||||||
`cio-org-id` header: rejects when `org.disableSignup` (403 `SIGNUP_DISABLED`, line 148)
|
|
||||||
or when `settings.signup.inviteOnly` and the email lacks an active invite (403
|
|
||||||
`INVITE_REQUIRED`, lines 97–113). Without the header: cloud passes through; **self‑hosted
|
|
||||||
requires org context** unless no org exists yet (bootstrap) or the email already belongs
|
|
||||||
to the org. Lines 75–91 (`canProceedWithoutOrgContext`).
|
|
||||||
- **User flow:** Signup form → POST `/api/auth/sign-up/*` (guarded, `app.ts:106`) → allowed
|
|
||||||
/ `SIGNUP_DISABLED` / `INVITE_REQUIRED` / `ORG_CONTEXT_REQUIRED`.
|
|
||||||
- **Edge cases (tester probes):** invite‑only org without invite; disabled signup;
|
|
||||||
self‑hosted with no `cio-org-id` and an existing org; unparseable body → 400 `INVALID_REQUEST`.
|
|
||||||
|
|
||||||
### 3.3 Onboarding & Organization Creation
|
|
||||||
- **Purpose:** create the user profile and first organization, then send a welcome email.
|
|
||||||
- **How it works:** `apps/api/src/services/onboarding.ts`. `createOrganizationWithOwner`
|
|
||||||
validates site‑name uniqueness (409 `SITENAME_EXISTS`, lines 36–40), creates org + admin
|
|
||||||
member in a transaction (lines 44–61), and on **self‑hosted** assigns the org an
|
|
||||||
`ENTERPRISE` plan with provider `selfhosted` (lines 64–77). **Self‑hosted blocks a second
|
|
||||||
org** (403, lines 28–33). `completeOnboarding` enqueues a `welcome` email (lines 121–134).
|
|
||||||
- **User flow:** Signup → `(auth)/onboarding` → enter name/org/site name → org+admin created → dashboard.
|
|
||||||
- **Edge cases:** duplicate site name (409 + Postgres `23505` fallback, lines 93–95);
|
|
||||||
second‑org attempt on self‑host; profile not found (404).
|
|
||||||
|
|
||||||
### 3.4 Organizations (Workspaces)
|
|
||||||
- **Purpose:** the top‑level tenant; owns courses, members, settings, plan, branding.
|
|
||||||
- **How it works:** `apps/api/src/routes/organization/organization.ts` (CRUD + settings);
|
|
||||||
`organization` table `schema.ts:1949` (siteName, settings, branding, `disableSignup`).
|
|
||||||
Members live in `organizationmember` (`schema.ts:1754`) with `roleId` FK to `role`.
|
|
||||||
- **User flow:** Org switcher → settings → update name/site/branding/signup policy.
|
|
||||||
`apps/dashboard/src/lib/features/ui/sidebar/org-sidebar/org-switcher.svelte`;
|
|
||||||
`apps/dashboard/src/lib/features/settings`.
|
|
||||||
- **Edge cases:** unique `siteName`; self‑hosted = single org (see §5); org settings drive
|
|
||||||
signup gating (§3.2).
|
|
||||||
|
|
||||||
### 3.5 Courses, Sections & Content
|
|
||||||
- **Purpose:** author and deliver courses (self‑paced, live‑class, compliance, public).
|
|
||||||
- **How it works:** course sub‑routers under `apps/api/src/routes/course/` — `course.ts`,
|
|
||||||
`section.ts`, `content.ts`, `lesson.ts`, `exercise.ts`, `people.ts`, `newsfeed.ts`,
|
|
||||||
`attendance.ts`, `compliance.ts`, `mark.ts`, `submission.ts`, `invite.ts`,
|
|
||||||
`lesson-language.ts`, `payment-request.ts`, `presign.ts`, `ai-tutor.ts`, `katex.ts`
|
|
||||||
(`index.ts` composes them). Course type enum `SELF_PACED|LIVE_CLASS|COMPLIANCE|PUBLIC`
|
|
||||||
(`schema.ts:26`). Course row `schema.ts:634`.
|
|
||||||
- **User flow:** Courses → New course → add sections/lessons → add exercises → publish →
|
|
||||||
students enroll. `apps/dashboard/src/routes/(app)/courses/[id]`.
|
|
||||||
- **Edge cases:** course type drives compliance/public behavior; locale enum (`schema.ts:27`,
|
|
||||||
10 languages) drives multilingual lessons.
|
|
||||||
|
|
||||||
### 3.6 Lessons & Multilingual Editor
|
|
||||||
- **Purpose:** lesson content with notes/slides/video, comments, translations.
|
|
||||||
- **How it works:** `lesson` (`schema.ts:956`), `lessonComment` (`schema.ts:516`),
|
|
||||||
`lessonCompletion` (`schema.ts:1490`), `lessonLanguage` + `…History` (`schema.ts:1706`,
|
|
||||||
`1732`). Routes: `apps/api/src/routes/course/lesson.ts`, `.../lesson-language.ts`.
|
|
||||||
PRDs `prd/lesson-edit [DONE]`, `prd/lessons-list [DONE]` confirm intent.
|
|
||||||
- **User flow:** Open lesson → edit content → add translation → student marks complete.
|
|
||||||
- **Edge cases:** language history retained; completion tracked per student.
|
|
||||||
|
|
||||||
### 3.7 Exercises, Question Types & Grading
|
|
||||||
- **Purpose:** quizzes/assessments with multiple question types and grading.
|
|
||||||
- **How it works:** `exercise` (`schema.ts:1175`), `exerciseSection` (`schema.ts:1217`),
|
|
||||||
`question`/`questionType`/`questionAnswer` (`schema.ts:1803–1913`), `submission`/
|
|
||||||
`submissionstatus` (`schema.ts:571,617`), `exerciseTemplate` (`schema.ts:2401`).
|
|
||||||
Routes `exercise.ts`, `submission.ts`, `mark.ts`. Shared types in `packages/question-types`.
|
|
||||||
PRDs: `exercise-question-types`, `exercise-sections [DONE]`, `video-recording-question-type [DONE]`.
|
|
||||||
- **User flow:** Author exercise → add questions (incl. video‑recording type) → student
|
|
||||||
submits → tutor/admin marks → status recorded.
|
|
||||||
- **Edge cases:** section‑based exercises; video‑recording answers (media pipeline);
|
|
||||||
`prd/selective-exercise-assignment` is **todo** (assign specific exercises to specific
|
|
||||||
students) — verify it is *not* yet available.
|
|
||||||
|
|
||||||
### 3.8 Enrollment, Course Invites & Secure Invites
|
|
||||||
- **Purpose:** add students to courses by email or shareable link, with audit trail.
|
|
||||||
- **How it works:** `apps/api/src/services/course/invite.ts`; `courseInvite`
|
|
||||||
(`schema.ts:1282`) + `courseInviteAudit` (`schema.ts:1342`); event‑type enum
|
|
||||||
`schema.ts:53`. Org invites: `organizationInvite` (`schema.ts:1384`, EMAIL/LINK enum
|
|
||||||
`schema.ts:71`) + `…Audit` (`schema.ts:1448`), routes `apps/api/src/routes/invite/invite.ts`.
|
|
||||||
PRD `secure-student-invites [DONE]`.
|
|
||||||
- **User flow:** Course → People → Invite (email or link) → invitee accepts via
|
|
||||||
`apps/dashboard/src/routes/invite/[hash]` or `/invite/link/[hash]` → enrolled.
|
|
||||||
- **Edge cases:** invite expiry/custom expiry (`AGENTS.md:231` references
|
|
||||||
`customExpiresAt`); link vs email; audit events recorded; invite‑only signup interplay (§3.2).
|
|
||||||
|
|
||||||
### 3.9 Compliance Training
|
|
||||||
- **Purpose:** deadline tracking, renewals/retake intervals, grace periods, completion records.
|
|
||||||
- **How it works:** `apps/api/src/routes/course/compliance.ts` and internal compliance
|
|
||||||
router `apps/api/src/routes/internal` (`AGENTS.md:87–98`); `courseCompletionRecord`
|
|
||||||
(`schema.ts:768`), `courseCompletionNotificationEvent` (`schema.ts:824`),
|
|
||||||
`courseCertificateIssue` (`schema.ts:850`). Course type `COMPLIANCE` (`schema.ts:26`).
|
|
||||||
PRD `compliance-training-platform [DONE]`.
|
|
||||||
- **User flow:** Mark course as compliance → set deadlines/renewal → learners complete →
|
|
||||||
completion record + certificate; renewal/grace events tracked.
|
|
||||||
- **Edge cases:** grace periods, renewal intervals, waivers (per README 21), notification events.
|
|
||||||
|
|
||||||
### 3.10 Certificates
|
|
||||||
- **Purpose:** issue branded completion certificates with custom IDs.
|
|
||||||
- **How it works:** `packages/certificates/src/render.ts` + `templates/` render the
|
|
||||||
certificate; `courseCertificateIssue` (`schema.ts:850`) records issuance.
|
|
||||||
- **User flow:** Learner completes course → certificate issued → downloadable/branded.
|
|
||||||
- **Edge cases:** custom certificate IDs (README 21); template selection.
|
|
||||||
|
|
||||||
### 3.11 Cohorts & Goals
|
|
||||||
- **Purpose:** group courses into cohorts with goals, members, and progress tracking.
|
|
||||||
- **How it works:** `apps/api/src/routes/cohort/cohort.ts`; tables `cohort`
|
|
||||||
(`schema.ts:2646`), `programCourse` (`2677`), `programMember` (`2704`), `programNewsfeed`
|
|
||||||
+`Comment` (`2739`,`2767`), `programGoal`/`…Assignment` (`2829`,`2878`) with goal‑type/
|
|
||||||
deadline/status enums (`2803–2828`), status enum `ACTIVE|INACTIVE|ARCHIVED` (`2644`).
|
|
||||||
Middlewares `cohort-member.ts`, `cohort-team-member.ts`. PRD `cohorts [DONE]`.
|
|
||||||
- **User flow:** Create cohort → add courses + members → set goals/deadlines → track
|
|
||||||
progress → cohort newsfeed. `apps/dashboard/src/routes/(app)/cohorts/[id]`.
|
|
||||||
- **Edge cases:** goal deadline kinds; assignment status; archived cohorts.
|
|
||||||
|
|
||||||
### 3.12 Community Q&A & Newsfeed
|
|
||||||
- **Purpose:** per‑course discussion (questions/answers) and announcements.
|
|
||||||
- **How it works:** `apps/api/src/routes/community/community.ts` with route‑specific
|
|
||||||
`middlewares/question-author-or-team` (`ARCHITECHTURE.md:241–252`); tables
|
|
||||||
`communityQuestion`/`communityAnswer` (`schema.ts:1558`,`1522`), `courseNewsfeed`/
|
|
||||||
`…Comment` (`1605`,`1632`). Comment‑author middlewares `newsfeed-comment-author-or-team.ts`,
|
|
||||||
`cohort-newsfeed-comment-author-or-team.ts`.
|
|
||||||
- **User flow:** Course → Community → ask/answer; team can moderate. Newsfeed posts +
|
|
||||||
comments at course and cohort level.
|
|
||||||
- **Edge cases:** author‑or‑team authorization (only author or course team can edit/delete).
|
|
||||||
|
|
||||||
### 3.13 AI Course Assistant (authoring chat)
|
|
||||||
- **Purpose:** in‑course AI that generates outlines, lesson content, and edits.
|
|
||||||
- **How it works:** `apps/api/src/routes/agent/{agent,history,runs}.ts`; `packages/ai-assistant`;
|
|
||||||
run tracking in `aiAgentRun`/`…Step`/`…Event` (`schema.ts:3163`,`3225`,`3259`),
|
|
||||||
conversation/model context/documents (`3083`,`3106`,`3138`). **Disabled until a provider
|
|
||||||
key is set**; `GET /agent/status` flips to `enabled:true` when any of OPENAI/GOOGLE/
|
|
||||||
ANTHROPIC key is present. `README.md:223–238`. Selected model persisted in `localStorage`
|
|
||||||
(`classroomio-ai-chat-model`), provider resolved server‑side; missing key → 503
|
|
||||||
`AI_NOT_CONFIGURED` (`README.md:236`).
|
|
||||||
- **User flow:** Course → AI button (only shown when enabled) → chat to build/edit course.
|
|
||||||
- **Edge cases:** no key → AI button hidden / 503; per‑model provider resolution; optional
|
|
||||||
Tinybird observability (`TINYBIRD_TOKEN`).
|
|
||||||
|
|
||||||
### 3.14 AI Lesson Tutor + Fair‑Use Caps
|
|
||||||
- **Purpose:** in‑lesson AI helper for learners, with usage caps.
|
|
||||||
- **How it works:** `apps/api/src/routes/course/ai-tutor.ts`,
|
|
||||||
`apps/api/src/routes/organization/ai-tutor.ts`; `aiTutorMessageCount` (`schema.ts:3013`)
|
|
||||||
+ `aiTutorCapEvent` (`3050`). Settings UI `apps/dashboard/src/lib/features/ai-tutor-settings`.
|
|
||||||
PRD `ai-tutor-fair-use`.
|
|
||||||
- **User flow:** Learner in lesson → ask tutor → counts toward cap → cap event when exceeded.
|
|
||||||
- **Edge cases:** per‑org cap configuration; cap‑exceeded behavior.
|
|
||||||
|
|
||||||
### 3.15 AI Credits / Token Billing
|
|
||||||
- **Purpose:** meter AI token usage and sell token packs.
|
|
||||||
- **How it works:** `aiTokenUsage` (`schema.ts:2919`), `aiCreditBalance` (`2950`),
|
|
||||||
`aiCreditPurchase` (`2974`); purchases recorded by Polar `order.paid` webhook →
|
|
||||||
`CreditPurchaseApiServer.recordPurchase`. `apps/dashboard/src/routes/api/polar/webhook/+server.ts`
|
|
||||||
→ 33–82; token‑pack constants `@cio/utils/plans` (`TOKEN_PACK.TOKENS_PER_UNIT`, line 53).
|
|
||||||
- **User flow:** Buy tokens (`/api/polar/buy-tokens`) → Polar checkout → webhook credits org balance.
|
|
||||||
- **Edge cases:** **cloud only** (Polar); quantity/metadata parsing; UUID validation of
|
|
||||||
`triggeredBy` (lines 61–66).
|
|
||||||
|
|
||||||
### 3.16 Course Import
|
|
||||||
- **Purpose:** create courses from a prompt, PDF, or existing course.
|
|
||||||
- **How it works:** `apps/api/src/routes/organization/course-import.ts`,
|
|
||||||
`apps/api/src/services/course-import`; `courseImportDraft` (`schema.ts:2603`) with source
|
|
||||||
enum `prompt|pdf|course` (`schema.ts:29`) and draft status `DRAFT|PUBLISHED|ARCHIVED` (`30`).
|
|
||||||
- **User flow:** Org → Import course → choose source → review draft → publish.
|
|
||||||
- **Edge cases:** draft lifecycle; PDF parsing pipeline (jobs/media).
|
|
||||||
|
|
||||||
### 3.17 Media Manager (video/docs, transcripts, HLS)
|
|
||||||
- **Purpose:** upload and manage media; transcode video to HLS; generate transcripts.
|
|
||||||
- **How it works:** routes `media/media.ts`, `transcripts.ts`, `hls.ts`, `course/presign.ts`
|
|
||||||
(S3 presigned uploads). Tables `asset`/`assetUsage` (`schema.ts:1028`,`1086`),
|
|
||||||
`mediaJob`/`jobStep`/`mediaTranscript`/`deadLetterJob` (`schema.ts:3297`,`3357`,`3393`,`3447`),
|
|
||||||
`videoTranscripts` (`916`). Jobs processed by `apps/jobs`. Dashboard
|
|
||||||
`apps/dashboard/src/lib/features/media`. PRD `media-manager [DONE]`. Storage via MinIO/S3
|
|
||||||
(`.env.example:77–89`).
|
|
||||||
- **User flow:** Media → upload → background transcode/transcribe → use asset in lesson.
|
|
||||||
- **Edge cases:** job retries / dead‑letter; HLS cross‑origin headers (`app.ts:48–54`);
|
|
||||||
Google Drive picker optional (`.env.example:99–101`).
|
|
||||||
|
|
||||||
### 3.18 Tags
|
|
||||||
- **Purpose:** categorize courses via tag groups/tags.
|
|
||||||
- **How it works:** `apps/api/src/routes/organization/tags.ts`; `tagGroup`/`tag`/
|
|
||||||
`tagAssignment` (`schema.ts:2158`,`2188`,`2225`). PRD `tags-feature [DONE]`.
|
|
||||||
- **User flow:** Org → Tags → create groups/tags → assign to courses → filter catalog.
|
|
||||||
|
|
||||||
### 3.19 Org Public Site (org‑site) & Theming
|
|
||||||
- **Purpose:** branded public catalog and course landing pages for an org.
|
|
||||||
- **How it works:** API `apps/api/src/routes/org-site` (mounted `/org-site/course`,
|
|
||||||
`app.ts:217`); dashboard public routes `apps/dashboard/src/routes/(org-site)/{course,courses}`.
|
|
||||||
Theme picker PRD `org-landing-page-theme-picker [DONE]`.
|
|
||||||
- **User flow:** Visitor → org public site → browse courses → view course → enroll/sign up.
|
|
||||||
- **Edge cases:** self‑host vs cloud routing differences (see §5); org branding/theme.
|
|
||||||
|
|
||||||
### 3.20 Custom Domains
|
|
||||||
- **Purpose:** map a customer's own domain to their org site.
|
|
||||||
- **How it works:** `apps/api/src/routes/domain/domain.ts` (mounted `/domain`, `app.ts:203`);
|
|
||||||
routing app `apps/tenant-router`. PRD `custom-domain-cloudflare [DONE]`.
|
|
||||||
- **User flow:** Org settings → add custom domain → Cloudflare mapping → site served on domain.
|
|
||||||
|
|
||||||
### 3.21 Course Widget Embed
|
|
||||||
- **Purpose:** embed an org's course catalog on any external website.
|
|
||||||
- **How it works:** API `apps/api/src/routes/widgets` (mounted `/widgets`, `app.ts:221`);
|
|
||||||
dashboard `apps/dashboard/src/routes/(app)/widgets/[widgetId]` + `widget-preview`; build
|
|
||||||
artifacts `apps/embeds` (`embeds:publish`, `README.md:256–257`). Tables `widget`/
|
|
||||||
`widgetCourse`/`widgetVersion` (`schema.ts:2255`,`2305`,`2336`) with status/layout/
|
|
||||||
selection enums (`42–52`). PRD `course-widget-embed [DONE]`.
|
|
||||||
- **User flow:** Widgets → create widget → choose courses/layout → publish → copy embed
|
|
||||||
snippet (`embed-preview.html`).
|
|
||||||
- **Edge cases:** draft vs published widget; manual vs published course selection mode.
|
|
||||||
|
|
||||||
### 3.22 Public REST API (v1) & Automation Keys
|
|
||||||
- **Purpose:** programmatic access for integrations/automation.
|
|
||||||
- **How it works:** `apps/api/src/routes/v1/{courses,audience}.ts` mounted at
|
|
||||||
`/public-api/v1` (`app.ts:218`, `v1/index.ts`). Auth via `automationKeyMiddleware` +
|
|
||||||
`automationKeyScopesMiddleware(['public_api:*'])` on every route; OpenAPI documented via
|
|
||||||
`hono-openapi`. Courses endpoints: list/create/get/update/delete, list students, export,
|
|
||||||
get/put structure (`v1/courses.ts`). Keys: `organizationApiKey` (`schema.ts:2539`, type
|
|
||||||
`mcp|api|zapier`), usage tracked in `organizationAutomationUsage` (`2574`); managed via
|
|
||||||
`apps/api/src/routes/organization/automation.ts`. PRD `public-api [DONE]`.
|
|
||||||
- **User flow:** Org → API keys → create key with scopes → call `/public-api/v1/...` with key.
|
|
||||||
- **Edge cases:** scope enforcement (403 on missing scope); usage categories `read|write|publish`
|
|
||||||
(`schema.ts:32`); per‑org isolation via `orgId` from key context.
|
|
||||||
|
|
||||||
### 3.23 MCP Server
|
|
||||||
- **Purpose:** AI‑native course authoring through the Model Context Protocol.
|
|
||||||
- **How it works:** `packages/mcp/src/{index.ts,api-client.ts,config.ts,tools/}`; published
|
|
||||||
as `@classroomio/mcp` (`README.md:258`). Talks to the public API using an automation key.
|
|
||||||
PRD `mcp-course-authoring [DONE]`.
|
|
||||||
- **User flow:** Configure MCP client with API key → AI tools create/update courses.
|
|
||||||
|
|
||||||
### 3.24 Enterprise SSO & Token Auth (licensed)
|
|
||||||
- **Purpose:** OIDC SSO and token‑based auth for enterprise self‑host.
|
|
||||||
- **How it works:** `apps/api/src/routes/organization/sso.ts`,
|
|
||||||
`apps/api/src/routes/sso/discovery.ts` (mounted `/organization/sso`, `/sso`, `app.ts:211,213`);
|
|
||||||
`organizationSsoConfig` (`schema.ts:2479`), provider enum `OKTA|GOOGLE_WORKSPACE|AUTH0`
|
|
||||||
(`schema.ts:2444`), `organizationAuthPolicy` (`2447`). Token auth:
|
|
||||||
`apps/api/src/routes/organization/token-auth.ts`, `organizationTokenAuth` (`2513`).
|
|
||||||
**License‑gated on self‑host** via `requireLicense('sso'|'token-auth')`
|
|
||||||
(`apps/api/src/middlewares/license.ts`). PRD `enterprise-sso [DONE]`.
|
|
||||||
- **User flow:** Org → SSO settings → register IdP (discovery fetches
|
|
||||||
`.well-known/openid-configuration`) → users sign in via IdP.
|
|
||||||
- **Edge cases:** trusted OIDC origins allow‑listed (`packages/db/src/constants.ts:14–24`);
|
|
||||||
cloud has no license restriction (see §5).
|
|
||||||
|
|
||||||
### 3.25 Licensing
|
|
||||||
- **Purpose:** gate Enterprise self‑host features behind a license key.
|
|
||||||
- **How it works:** `apps/api/src/routes/license.ts` (mounted `/license`, `app.ts:209`),
|
|
||||||
`apps/api/src/services/license.ts` (`isFeatureLicensed`), feature ids `sso`,
|
|
||||||
`token-auth`, `no-tracking` (`packages/utils/src/license/constants.ts:8–10`),
|
|
||||||
plan→feature mapping `packages/utils/src/license/plan-features.ts`. Dashboard side
|
|
||||||
`apps/dashboard/src/lib/features/license/api/license.svelte.ts` (`licenseApi.hasAccess`).
|
|
||||||
- **User flow:** Set `LICENSE_KEY` (`.env.example:117–118`) → licensed features unlock.
|
|
||||||
- **Edge cases:** `requireLicense` is a **no‑op on cloud** (`license.ts:15–19`).
|
|
||||||
|
|
||||||
### 3.26 Billing (Polar) — cloud
|
|
||||||
- **Purpose:** paid subscriptions and AI token packs.
|
|
||||||
- **How it works:** `apps/dashboard/src/routes/api/polar/{subscribe,buy-tokens,portal,webhook}`.
|
|
||||||
Webhook handles `order.paid` (token packs → credit balance) and `subscription.*`
|
|
||||||
(create/update/cancel org plan). `webhook/+server.ts` → 30–168. Plans enum
|
|
||||||
`EARLY_ADOPTER|ENTERPRISE|BASIC` (`schema.ts:28`), org plan stored in `organizationPlan`
|
|
||||||
(`schema.ts:1667`).
|
|
||||||
- **User flow:** Upgrade → Polar checkout → webhook activates/updates plan; manage via portal.
|
|
||||||
- **Edge cases:** **cloud only**; subscription cancel → `cancelOrgPlan`; webhook secret
|
|
||||||
`POLAR_WEBHOOK_SECRET` required.
|
|
||||||
|
|
||||||
### 3.27 Analytics
|
|
||||||
- **Purpose:** usage analytics for orgs/courses.
|
|
||||||
- **How it works:** `apps/api/src/routes/dash` + `apps/api/src/services/analytics`; tables
|
|
||||||
`analyticsLoginEvents`, `analyticsPageEvent`, `analyticsOrgDaily`, `analyticsCourseDaily`,
|
|
||||||
`analyticsCountryDaily` (`schema.ts:167–285`). Dashboard `apps/dashboard/src/lib/features/analytics`.
|
|
||||||
Client tracking (PostHog/Umami) gated by deployment mode / license (§5).
|
|
||||||
- **User flow:** Org → Analytics dashboard → view login/page/course metrics.
|
|
||||||
|
|
||||||
### 3.28 Email / Notifications & Background Jobs
|
|
||||||
- **Purpose:** transactional email and async processing.
|
|
||||||
- **How it works:** `apps/api/src/routes/mail`, templates in `packages/email`,
|
|
||||||
enqueued via `enqueueTransactionalEmail` (`apps/api/src/services/jobs`,
|
|
||||||
`onboarding.ts:128`). Workers in `apps/jobs`; BullMQ dev dashboard at `/admin/queues`
|
|
||||||
(`apps/api/src/routes/admin/queues.ts`, `app.ts:232–234`). Email providers: Zoho/ZeptoMail
|
|
||||||
or SMTP (`.env.example:62–74`).
|
|
||||||
- **User flow:** trigger (signup, invite, completion) → email queued → worker sends.
|
|
||||||
- **Edge cases:** idempotency keys (`onboarding.ts:133`); dead‑letter jobs (`schema.ts:3447`).
|
|
||||||
- **Note:** an in‑app **notification system** is `prd/notification-system` **todo** — not shipped.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Roles & Responsibilities
|
|
||||||
|
|
||||||
**Org roles** are integers: `ADMIN=1`, `TUTOR=2`, `STUDENT=3`
|
|
||||||
(`packages/utils/src/constants/roles.ts:1–4`). They are stored on
|
|
||||||
`organizationmember.roleId` (`schema.ts:1767`, FK to `role` table `schema.ts:1913`) and
|
|
||||||
surfaced into the session as an `orgRoles` map keyed by org id (`apps/api/src/app.ts:88`).
|
|
||||||
RBAC middlewares read `orgRoles[orgId]` (org id from the `cio-org-id` header) — **no
|
|
||||||
per‑request DB lookup** (`apps/api/src/middlewares/org-team-member.ts:5–53`).
|
|
||||||
|
|
||||||
| Role | Can do | Cannot do | Enforced by |
|
|
||||||
|------|--------|-----------|-------------|
|
|
||||||
| **ADMIN** (1) | Full org control: settings, members, billing, courses, SSO, API keys, all team actions | — | `org-admin.ts`, `org-team-member.ts` (admin∈team) |
|
|
||||||
| **TUTOR** (2) | Team actions: author/manage courses, grade, moderate community/newsfeed | Org admin‑only ops (org settings, billing, member roles) | `org-team-member.ts:44` (ADMIN or TUTOR), `course-team-member.ts` |
|
|
||||||
| **STUDENT** (3) | Enroll, learn, submit exercises, post community Q&A, use tutor | Authoring/grading/team/admin actions | absence of team/admin role → 403 |
|
|
||||||
| **Guest / anonymous** | Public org sites, public courses, embeds, public API (with key) | Any authed action | session = null path (`app.ts:77–84`); public CORS paths (`app.ts:60–67`) |
|
|
||||||
|
|
||||||
**Resource‑scoped middlewares** (`apps/api/src/middlewares/`):
|
|
||||||
- `authMiddleware` — requires a session (`auth.ts`).
|
|
||||||
- `orgMemberMiddleware` — must be org member; `orgTeamMemberMiddleware` — ADMIN|TUTOR;
|
|
||||||
`orgAdminMiddleware` — ADMIN only (`ARCHITECHTURE.md:233–237`).
|
|
||||||
- `courseMemberMiddleware` / `courseTeamMemberMiddleware` — course‑level membership/team.
|
|
||||||
- `cohortMemberMiddleware` / `cohortTeamMemberMiddleware` — cohort-level.
|
|
||||||
- `apiKeyMiddleware` / `automationKeyMiddleware` (+ `automationKeyScopesMiddleware`) —
|
|
||||||
machine auth for webhooks/public API; `authOrApiKeyMiddleware`,
|
|
||||||
`authOrAutomationKeyMiddleware`, and `*-or-automation-key`/`*-or-api-key` variants allow
|
|
||||||
either a session or a key.
|
|
||||||
- `requireLicense(feature)` — self‑host license gate (`license.ts`).
|
|
||||||
- `workspaceNotReadonlyMiddleware` — blocks writes on read‑only workspaces
|
|
||||||
(`workspace-not-readonly.ts`).
|
|
||||||
- `signupGuard` — signup rules (§3.2). `rateLimiter` — global rate limit (`app.ts:98`).
|
|
||||||
- Route‑specific author/team guards: `question-author-or-team`,
|
|
||||||
`newsfeed-comment-author-or-team`, `cohort-newsfeed-comment-author-or-team`.
|
|
||||||
|
|
||||||
**Public API scopes:** automation keys carry scopes like `public_api:*`, enforced per route
|
|
||||||
(`apps/api/src/routes/v1/courses.ts:47`); usage categorized `read|write|publish`
|
|
||||||
(`schema.ts:32`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Deployment Modes — Self‑Hosted vs Cloud
|
|
||||||
|
|
||||||
The single switch is **`PUBLIC_IS_SELFHOSTED`** (`=true` ⇒ self‑hosted). Source of truth:
|
|
||||||
`.env.example:26` (Docker stack), `apps/dashboard/.env.example`, and the local‑dev value
|
|
||||||
`apps/dashboard/.env` (`PUBLIC_IS_SELFHOSTED=true`, `README.md:155`). The flag is read in
|
|
||||||
both the API/db layer (`process.env.PUBLIC_IS_SELFHOSTED`) and the dashboard
|
|
||||||
(`$env/static/public`). Every behavioral difference below is traced to a flag check.
|
|
||||||
|
|
||||||
### 5.1 Self‑Hosted mode (`PUBLIC_IS_SELFHOSTED=true`)
|
|
||||||
- **Setup:** Docker Compose full stack — `cp .env.example .env` then
|
|
||||||
`./run-docker-full-stack.sh` (auto‑generates `PRIVATE_SERVER_KEY`, `BETTER_AUTH_SECRET`);
|
|
||||||
`README.md:240–249`, `docker-compose.yaml`, `docker/Dockerfile.{api,dashboard}`,
|
|
||||||
`docker/docs/SELF_HOST.md`. Bundled Postgres, Redis, MinIO.
|
|
||||||
- **Single organization:** org creation **blocks a second org**
|
|
||||||
(`apps/api/src/services/onboarding.ts:28–33`); the new org is auto‑assigned an
|
|
||||||
`ENTERPRISE` plan with provider `selfhosted` (`onboarding.ts:64–77`). The "add org" UI is
|
|
||||||
**hidden** (`apps/dashboard/src/routes/(app)/org/[slug]/+layout.svelte:29–31`) and the
|
|
||||||
org‑site→LMS redirect is **skipped** (`.../+layout.server.ts:16`). PRD `self-host-single-org.md`.
|
|
||||||
- **Signup:** requires org context unless bootstrapping the first org or the email already
|
|
||||||
belongs to the org (`signup-guard.ts:75–91`).
|
|
||||||
- **Licensing:** Enterprise features (`sso`, `token-auth`, `no-tracking`) are **gated by
|
|
||||||
`LICENSE_KEY`** — `requireLicense` actively checks (`middlewares/license.ts:15–22`);
|
|
||||||
`.env.example:117–118`.
|
|
||||||
- **Tracking:** cloud analytics (PostHog/Umami) are **disabled**
|
|
||||||
(`apps/dashboard/src/lib/utils/functions/appSetup.ts:23–27`); a `no-tracking` license also
|
|
||||||
suppresses tracking (`appSetup.ts:29–37`).
|
|
||||||
- **URLs/links:** `BASE_URL` and invite/email links use `DASHBOARD_ORIGIN` when self‑hosted
|
|
||||||
(`packages/db/src/constants.ts:36–37`; `.env.example:32–35`). Dashboard SSR calls the API
|
|
||||||
internally via `PRIVATE_SERVER_URL` (`.env.example:28–30`).
|
|
||||||
- **Storage/email:** MinIO + Zoho/SMTP from `.env` (`.env.example:62–89`).
|
|
||||||
|
|
||||||
### 5.2 Cloud mode (`PUBLIC_IS_SELFHOSTED` ≠ `true`)
|
|
||||||
- **Multi‑org:** users can create/switch multiple orgs (no single‑org block); "add org" UI
|
|
||||||
shown (`+layout.svelte:29`).
|
|
||||||
- **Billing:** Polar subscriptions + AI token packs are active
|
|
||||||
(`apps/dashboard/src/routes/api/polar/*`); plan set via subscription webhooks
|
|
||||||
(`webhook/+server.ts`). License gate is a **no‑op** — cloud has no license restrictions
|
|
||||||
(`middlewares/license.ts:15–19`).
|
|
||||||
- **Tracking:** PostHog/Umami/UserJot analytics enabled (`appSetup.ts:17–27`).
|
|
||||||
- **Signup:** org context optional unless org settings restrict it (`signup-guard.ts:132–139`).
|
|
||||||
- **Infra:** hosted Postgres/Redis/S3; dashboard deployed on Cloudflare Workers
|
|
||||||
(`apps/dashboard/wrangler.jsonc`, `svelte.config.js`).
|
|
||||||
|
|
||||||
### 5.3 Mode‑exclusive features (summary)
|
|
||||||
| Concern | Self‑Hosted | Cloud |
|
|
||||||
|---|---|---|
|
|
||||||
| Number of orgs | One only (`onboarding.ts:28`) | Many |
|
|
||||||
| Enterprise SSO / token‑auth | Requires `LICENSE_KEY` (`license.ts`) | Always allowed (no license) |
|
|
||||||
| Billing (Polar) | Not used (plan = selfhosted ENTERPRISE) | Subscriptions + token packs |
|
|
||||||
| Analytics tracking | Off (unless… still off) (`appSetup.ts:24`) | On |
|
|
||||||
| API base / links | `DASHBOARD_ORIGIN` (`constants.ts:37`) | `PUBLIC_SERVER_URL` |
|
|
||||||
| Add‑org modal | Hidden (`+layout.svelte:29`) | Shown |
|
|
||||||
|
|
||||||
> Both modes exist in source and share one codebase; **neither mode is
|
|
||||||
> `⚠️ NOT FOUND`.** The product is one app with flag‑driven divergence.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. End‑to‑End Flow Diagrams (text)
|
|
||||||
|
|
||||||
**A. Signup → Onboarding → Workspace**
|
|
||||||
```
|
|
||||||
Login/Signup (apps/dashboard/.../(auth)/signup)
|
|
||||||
→ POST /api/auth/sign-up/* [signupGuard] (app.ts:106; signup-guard.ts)
|
|
||||||
→ Better Auth creates user + profile (auth.ts; create-profile.ts)
|
|
||||||
→ (auth)/onboarding: createOrganizationWithOwner (onboarding.ts:19)
|
|
||||||
· self-host: block 2nd org, assign ENTERPRISE plan
|
|
||||||
→ completeOnboarding → enqueue 'welcome' email (onboarding.ts:121)
|
|
||||||
→ Dashboard /lms or /org/[slug]
|
|
||||||
```
|
|
||||||
|
|
||||||
**B. Author a course**
|
|
||||||
```
|
|
||||||
Courses → New course (course.ts)
|
|
||||||
→ add sections (section.ts) → lessons (lesson.ts) + translations (lesson-language.ts)
|
|
||||||
→ add exercises/questions (exercise.ts, question-types)
|
|
||||||
→ [optional] AI Assistant builds content (agent/*; requires provider key)
|
|
||||||
→ publish → appears on org site / widget
|
|
||||||
```
|
|
||||||
|
|
||||||
**C. Student learns → certificate (compliance)**
|
|
||||||
```
|
|
||||||
Invite (course/invite.ts) → accept /invite/[hash]
|
|
||||||
→ enroll (people.ts) → study lessons → mark complete (lessonCompletion)
|
|
||||||
→ submit exercises (submission.ts) → tutor grades (mark.ts)
|
|
||||||
→ completion recorded (courseCompletionRecord schema.ts:768)
|
|
||||||
→ certificate issued (packages/certificates; courseCertificateIssue schema.ts:850)
|
|
||||||
→ compliance: deadlines/renewals tracked (course/compliance.ts; internal/*)
|
|
||||||
```
|
|
||||||
|
|
||||||
**D. Public API automation (integration)**
|
|
||||||
```
|
|
||||||
Org → create automation key with scopes (organization/automation.ts; organizationApiKey)
|
|
||||||
→ external client: GET/POST /public-api/v1/courses
|
|
||||||
[automationKeyMiddleware + automationKeyScopesMiddleware('public_api:*')]
|
|
||||||
→ service resolves orgId from key → returns course data (v1/courses.ts; services/v1)
|
|
||||||
```
|
|
||||||
|
|
||||||
**E. Cloud upgrade / buy AI tokens (cloud only)**
|
|
||||||
```
|
|
||||||
Upgrade or Buy tokens (api/polar/subscribe | buy-tokens)
|
|
||||||
→ Polar checkout
|
|
||||||
→ Polar webhook (api/polar/webhook/+server.ts)
|
|
||||||
· subscription.created → createOrgPlan
|
|
||||||
· order.paid (token_pack) → recordPurchase → aiCreditBalance
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Open Questions & Gaps
|
|
||||||
|
|
||||||
1. **Outbound webhooks — doc vs code mismatch.** `README.md:42–43` advertises
|
|
||||||
"REST API + Webhooks … receive events (`certificate.issued`, `enrollment.completed`)",
|
|
||||||
but the only webhook code is **inbound Polar billing**
|
|
||||||
(`apps/dashboard/src/routes/api/polar/webhook`) and `prd/webhooks` is **todo**
|
|
||||||
(`PRD-TRACKER.md:24`). No outbound event‑emission/delivery system was found in
|
|
||||||
`apps/api/src` or `packages/db/src`. **Code wins: outbound webhooks appear unimplemented.**
|
|
||||||
QA should not test event delivery as a shipped feature. ⚠️
|
|
||||||
2. **SCORM support** — README does not promise it, but `prd/scorm-support` is **todo**
|
|
||||||
(`PRD-TRACKER.md:16`). No SCORM code located. ⚠️ Treat as not shipped.
|
|
||||||
3. **In‑app notification system** — `prd/notification-system` is **todo**
|
|
||||||
(`PRD-TRACKER.md:9`). Transactional *email* exists (§3.28); in‑app notifications appear
|
|
||||||
unbuilt. Verify before demoing. ⚠️
|
|
||||||
4. **Selective exercise assignment** — `prd/selective-exercise-assignment` is **todo**
|
|
||||||
(`PRD-TRACKER.md:15`). Assigning specific exercises to specific students may not exist;
|
|
||||||
confirm against `exercise.ts` behavior. ⚠️
|
|
||||||
5. **Course templates / cohorts / import variants** — `prd/course-templates`,
|
|
||||||
`prd/course-cohorts`, `prd/course-import` are **todo** (`PRD-TRACKER.md:10,12,13`),
|
|
||||||
yet related schema exists (`exerciseTemplate` `schema.ts:2401`; `courseImportDraft`
|
|
||||||
`schema.ts:2603`; cohorts are DONE). Scope of what's actually wired end‑to‑end vs.
|
|
||||||
schema‑only should be verified per feature before writing test cases. ⚠️
|
|
||||||
6. **Anthropic in AI picker** — code supports Anthropic, but it is "not currently in the
|
|
||||||
picker UI" (`README.md:231`, `.env.example:112`). The picker exposes Gemini + GPT‑4o;
|
|
||||||
note the `.env.example:111` "Gemini 3 Flash" vs `README.md:228` "Gemini 2.5 Flash"
|
|
||||||
wording inconsistency — verify the actual model id in `packages/utils/src/agent-models`.
|
|
||||||
7. **Two operating sub‑modes within self‑host** (single‑org bootstrap vs. licensed
|
|
||||||
Enterprise) interact with `signupGuard` and `requireLicense`. Exact UX when
|
|
||||||
`LICENSE_KEY` is absent on self‑host (SSO/token‑auth hidden vs. 403) should be confirmed
|
|
||||||
in the dashboard `license` feature before recording a self‑host SSO demo.
|
|
||||||
8. **Tenant‑router / domain routing** (`apps/tenant-router`, `/domain`) was inventoried but
|
|
||||||
not deeply traced (infra scope). Custom‑domain happy‑path is PRD‑DONE; failure/SSL
|
|
||||||
provisioning states are unverified here.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Appendix — Supporting Infrastructure (light)
|
|
||||||
|
|
||||||
These exist in source but are infra/dev tooling rather than demoable product features:
|
|
||||||
|
|
||||||
- `apps/jobs` — BullMQ worker process; consumes media/email queues
|
|
||||||
(paired with `apps/api/src/routes/jobs`, `mediaJob`/`jobStep` tables). Dev queue UI at
|
|
||||||
`/admin/queues` (`apps/api/src/routes/admin/queues.ts`).
|
|
||||||
- `apps/tenant-router` — multi‑tenant/custom‑domain request routing (backs §3.20).
|
|
||||||
- `apps/course-app` / `packages/course-app` — course‑app runtime modules
|
|
||||||
(`pnpm-workspace.yaml:4`).
|
|
||||||
- `apps/docs` — documentation site (`pnpm docs:dev`).
|
|
||||||
- `apps/website` — marketing site (`pnpm website:dev`).
|
|
||||||
- `packages/core` — shared config incl. env access (`packages/core/src/config/env.ts`,
|
|
||||||
used by `signup-guard.ts`, `license.ts`, `onboarding.ts`).
|
|
||||||
- `packages/storybook` — UI component stories (`pnpm storybook:dev`).
|
|
||||||
- `packages/analytics` — analytics helpers (paired with `apps/api/src/services/analytics`).
|
|
||||||
- `packages/tsconfig` — shared TS config.
|
|
||||||
- `packages/ui` — shared Svelte component library (`@cio/ui`); theme classes require `ui:`
|
|
||||||
prefix in dashboard (`AGENTS.md:313`).
|
|
||||||
- `packages/utils` — shared Zod validation, plans, license, agent‑models, constants.
|
|
||||||
- `packages/db` — Drizzle schema/queries/migrations/seed; `pnpm --filter @cio/db db:setup:seed`
|
|
||||||
seeds demo data and the demo login `[email protected]` / `123456` (`README.md:215–219`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### How this audit was verified
|
|
||||||
Every section traces to files read during the audit: `README.md`, `ARCHITECHTURE.md`,
|
|
||||||
`AGENTS.md`, `.env.example`, `PRD-TRACKER.md`, `apps/api/src/app.ts`,
|
|
||||||
`packages/db/src/schema.ts`, the role/RBAC middlewares, `signup-guard.ts`,
|
|
||||||
`onboarding.ts`, `appSetup.ts`, the Polar webhook, the v1 public‑API router, and the
|
|
||||||
license constants/middleware. No application was run and no tests were executed —
|
|
||||||
functional testing is deferred to the QA phase per the audit brief.
|
|
||||||
@@ -255,12 +255,12 @@ Notes:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # copy env template, edit for your domain
|
cp .env.example .env # copy env template, edit for your domain
|
||||||
./run-docker-full-stack.sh
|
./classroomio.sh start # pulls pre-built images; use --build to build from source
|
||||||
```
|
```
|
||||||
|
|
||||||
The script reads root `.env` via `docker compose --env-file .env` and auto-generates a secure `PRIVATE_SERVER_KEY` (and `BETTER_AUTH_SECRET`) when missing.
|
The script reads root `.env` via `docker compose --env-file .env` and auto-generates a secure `PRIVATE_SERVER_KEY` (and `BETTER_AUTH_SECRET`) when missing. Run `./classroomio.sh` with no arguments for the full lifecycle menu (install, start, stop, restart, upgrade, logs, backup).
|
||||||
|
|
||||||
See [`.env.example`](.env.example) for the full list of environment variables with required/optional grouping, and [`docker/docs/SELF_HOST.md`](docker/docs/SELF_HOST.md) for the complete Docker self-hosting guide. To raise upload caps (documents, videos, assignment files), set the `UPLOAD_MAX_*_MB` vars in `.env` — see the [docs](https://classroomio.com/docs/self-hosted/docker#optional--upload-file-size-limits).
|
See [`.env.example`](.env.example) for the full list of environment variables with required/optional grouping, and [`docker/docs/SELF_HOST.md`](docker/docs/SELF_HOST.md) for the complete Docker self-hosting guide. To raise upload caps (documents, videos, assignment files), set the `UPLOAD_MAX_*_MB` vars in `.env` — see the [docs](https://classroomio.com/docs/self-hosted/configuration/storage#upload-file-size-limits).
|
||||||
|
|
||||||
## Publishing
|
## Publishing
|
||||||
|
|
||||||
|
|||||||
@@ -168,8 +168,14 @@ export default defineConfig({
|
|||||||
root: '/self-hosted',
|
root: '/self-hosted',
|
||||||
items: [
|
items: [
|
||||||
'/self-hosted',
|
'/self-hosted',
|
||||||
|
'/self-hosted/101',
|
||||||
|
'/self-hosted/architecture',
|
||||||
{
|
{
|
||||||
label: 'Platforms',
|
label: 'Install',
|
||||||
|
display: 'group',
|
||||||
|
// Pinned open — it's what most visitors came for. The other groups
|
||||||
|
// start collapsed (and auto-expand when they contain the current page).
|
||||||
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
'/self-hosted/docker',
|
'/self-hosted/docker',
|
||||||
'/self-hosted/digitalocean',
|
'/self-hosted/digitalocean',
|
||||||
@@ -179,7 +185,9 @@ export default defineConfig({
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Configuration',
|
label: 'Configure',
|
||||||
|
display: 'group',
|
||||||
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
'/self-hosted/configuration',
|
'/self-hosted/configuration',
|
||||||
'/self-hosted/configuration/secrets',
|
'/self-hosted/configuration/secrets',
|
||||||
@@ -187,10 +195,21 @@ export default defineConfig({
|
|||||||
'/self-hosted/configuration/storage',
|
'/self-hosted/configuration/storage',
|
||||||
'/self-hosted/configuration/csp',
|
'/self-hosted/configuration/csp',
|
||||||
'/self-hosted/configuration/integrations',
|
'/self-hosted/configuration/integrations',
|
||||||
'/self-hosted/configuration/sso',
|
'/self-hosted/enterprise'
|
||||||
'/self-hosted/configuration/versions'
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Manage',
|
||||||
|
display: 'group',
|
||||||
|
collapsed: true,
|
||||||
|
items: ['/self-hosted/configuration/versions', '/self-hosted/backups']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Troubleshoot',
|
||||||
|
display: 'group',
|
||||||
|
collapsed: true,
|
||||||
|
items: ['/self-hosted/troubleshooting']
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Set `LICENSE_KEY` in the API service environment.
|
|||||||
Local development: `apps/api/.env`.
|
Local development: `apps/api/.env`.
|
||||||
Docker Compose: repository root `.env`.
|
Docker Compose: repository root `.env`.
|
||||||
If you do not use Enterprise-only features, leave `LICENSE_KEY` unset.
|
If you do not use Enterprise-only features, leave `LICENSE_KEY` unset.
|
||||||
See [Self Hosting → SSO & licensing](/self-hosted/configuration/sso) for per-platform placement.
|
See [Self Hosting → Enterprise & licensing](/self-hosted/enterprise) for per-platform placement.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Supported Providers
|
## Supported Providers
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
title: Self-hosting 101
|
||||||
|
description: What you get when you self-host ClassroomIO, how the stack fits together, and how self-hosted differs from the cloud product.
|
||||||
|
---
|
||||||
|
|
||||||
|
ClassroomIO is an open-source learning platform you can run entirely on your own infrastructure.
|
||||||
|
This page explains what you're actually deploying before you pick an
|
||||||
|
[install path](/self-hosted#which-should-i-choose).
|
||||||
|
|
||||||
|
## What you get
|
||||||
|
|
||||||
|
The full product: the same code that powers the cloud version. Self-hosted, you run:
|
||||||
|
|
||||||
|
- The **dashboard** your team and students use (courses, exercises, communities, certificates).
|
||||||
|
- The **API** behind it.
|
||||||
|
- A background **jobs worker** for the heavy lifting: video processing, transcription, AI course
|
||||||
|
generation, and email sending.
|
||||||
|
- The infrastructure they need: **Postgres** (database), **Redis** (queues and caching), and
|
||||||
|
**MinIO** (S3-compatible object storage for uploads, swappable for AWS S3 or Cloudflare R2).
|
||||||
|
|
||||||
|
That's the ~7 containers the Docker stack starts (MinIO ships with a small one-shot init container
|
||||||
|
that creates its buckets). They're deliberately separate services: the parts that spike CPU and RAM
|
||||||
|
(video transcodes) are isolated from the parts that serve your users. How they talk to each other
|
||||||
|
is on the [Architecture](/self-hosted/architecture) page; what hardware they need is in the
|
||||||
|
[server requirements](/self-hosted#server-requirements).
|
||||||
|
|
||||||
|
## Self-hosted vs cloud
|
||||||
|
|
||||||
|
| | Cloud (app.classroomio.com) | Self-hosted |
|
||||||
|
|---|---|---|
|
||||||
|
| Infrastructure | Managed for you | Yours — your server, your domain |
|
||||||
|
| Your data | Hosted by ClassroomIO | Stays on your infrastructure |
|
||||||
|
| Updates | Automatic | You choose when ([pin a version](/self-hosted/configuration/versions)) |
|
||||||
|
| Organizations | Multiple per account, plan-based feature tiers | Single organization with all core features |
|
||||||
|
| Enterprise features (SSO, token auth, no-tracking) | On the Enterprise plan | Via a [license key](/self-hosted/enterprise) |
|
||||||
|
|
||||||
|
Self-hosted mode is controlled by one flag, `PUBLIC_IS_SELFHOSTED=true`, which is already set in
|
||||||
|
the shipped Docker configuration. It locks the instance to a single organization with the full core
|
||||||
|
feature set, and points invite links and emails at your own domain.
|
||||||
|
|
||||||
|
**About telemetry:** self-hosted mode disables error reporting (Sentry) and the in-app feedback
|
||||||
|
widget out of the box. Product analytics (PostHog, Umami) are only disabled by the `no-tracking`
|
||||||
|
enterprise license feature. See [Enterprise & licensing](/self-hosted/enterprise).
|
||||||
|
|
||||||
|
## How licensing works
|
||||||
|
|
||||||
|
ClassroomIO is **open core** under the [AGPL-3.0 license](https://github.com/classroomio/classroomio/blob/main/LICENSE):
|
||||||
|
|
||||||
|
- **Everything an LMS needs is free:** courses, students, uploads, video, AI features,
|
||||||
|
communities, certificates. No seat limits, no artificial caps. You never need a license key to
|
||||||
|
run a complete platform.
|
||||||
|
- **A small set of enterprise features** (SSO, token auth, and no-tracking mode) are gated behind
|
||||||
|
a paid `LICENSE_KEY`. They exist so organizations with compliance requirements can fund the
|
||||||
|
project. Details on the [Enterprise & licensing](/self-hosted/enterprise) page.
|
||||||
|
|
||||||
|
There is exactly one codebase and one set of Docker images. A license key unlocks features in the
|
||||||
|
same build you already run, so you never migrate editions.
|
||||||
|
|
||||||
|
## Where to next
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="Pick an install path" href="/self-hosted" icon="server">
|
||||||
|
Docker, DigitalOcean, Railway, Coolify, or Dokploy.
|
||||||
|
</Card>
|
||||||
|
<Card title="Architecture" href="/self-hosted/architecture" icon="network">
|
||||||
|
The services, how they connect, and why the jobs worker needs RAM.
|
||||||
|
</Card>
|
||||||
|
<Card title="Configuration" href="/self-hosted/configuration" icon="settings">
|
||||||
|
Every environment variable the stack reads.
|
||||||
|
</Card>
|
||||||
|
<Card title="Enterprise & licensing" href="/self-hosted/enterprise" icon="id-card">
|
||||||
|
SSO, token auth, no-tracking, and how license validation works.
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
title: Architecture
|
||||||
|
description: The services in a self-hosted ClassroomIO stack, how they authenticate to each other, and why the jobs worker drives your RAM requirement.
|
||||||
|
---
|
||||||
|
|
||||||
|
Every install path (Docker, DigitalOcean, Railway, Coolify, Dokploy) runs the same topology, so
|
||||||
|
this page applies no matter which one you picked.
|
||||||
|
|
||||||
|
## The services
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
B[Browser] -->|HTTPS| D[dashboard :3082]
|
||||||
|
D -->|Bearer PRIVATE_SERVER_KEY| A[api :3081]
|
||||||
|
A --> P[(postgres)]
|
||||||
|
A --> R[(redis)]
|
||||||
|
A --> M[(minio / S3)]
|
||||||
|
J[jobs worker] --> R
|
||||||
|
J --> P
|
||||||
|
J --> M
|
||||||
|
```
|
||||||
|
|
||||||
|
| Service | Image | Port | Role |
|
||||||
|
|---------|-------|------|------|
|
||||||
|
| `dashboard` | `classroomio/dashboard` | 3082 | The SvelteKit web app. Server-side renders pages and proxies browser API calls, so user traffic is same-origin. |
|
||||||
|
| `api` | `classroomio/api` | 3081 | The backend. Runs **database migrations on boot**, then serves the API. |
|
||||||
|
| `jobs` | `classroomio/jobs` | — | Background worker (BullMQ). Drains the Redis queues: video processing, transcription, AI course generation, emails, notifications, maintenance. Ships with ffmpeg. |
|
||||||
|
| `postgres` | `postgres:16-alpine` | internal | The database. All state lives here (volume `postgres-data`). |
|
||||||
|
| `redis` | `redis:7-alpine` | internal | Job queues and caching, persisted with AOF (volume `redis-data`). |
|
||||||
|
| `minio` | `minio/minio` | 9000/9001 | S3-compatible object storage for uploads (volume `minio-data`). Swappable for AWS S3 / Cloudflare R2. |
|
||||||
|
| `minio-init` | `minio/mc` | — | One-shot: creates the `videos`, `documents`, and `media` buckets, makes `media` publicly downloadable, then exits. |
|
||||||
|
|
||||||
|
The app services are pinned by one variable, `CIO_VERSION`. See
|
||||||
|
[Versions & upgrades](/self-hosted/configuration/versions).
|
||||||
|
|
||||||
|
## Startup order
|
||||||
|
|
||||||
|
The compose file encodes the dependency chain with healthchecks:
|
||||||
|
|
||||||
|
1. **postgres** and **redis** start first. Postgres only reports healthy once it answers a real
|
||||||
|
query (`SELECT 1`), not just `pg_isready`, because Postgres can accept connections while still
|
||||||
|
in crash recovery, which would surface as
|
||||||
|
[57P03 sign-in errors](/self-hosted/troubleshooting#sign-in-fails-with-57p03-right-after-startup).
|
||||||
|
2. **api** starts, runs any pending database migrations, then listens. A failed migration exits the
|
||||||
|
container instead of serving a half-migrated database.
|
||||||
|
3. **dashboard** starts only after the API is healthy.
|
||||||
|
|
||||||
|
This is why a first boot takes a minute or two: the API is migrating before anything responds.
|
||||||
|
|
||||||
|
## The api ↔ dashboard handshake
|
||||||
|
|
||||||
|
The dashboard's server side calls the API over the internal container network
|
||||||
|
(`PRIVATE_SERVER_URL`, default `http://api:3081`) and authenticates every request with:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <PRIVATE_SERVER_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
The API rejects requests where the key is missing or wrong (401), and refuses to operate without
|
||||||
|
one configured (500). Two consequences for operators:
|
||||||
|
|
||||||
|
- **The value must be identical in the api and dashboard services.** The compose file passes the
|
||||||
|
same `.env` value to both, and `./classroomio.sh` generates a strong one automatically. On PaaS
|
||||||
|
platforms where you set env per service, a mismatch is the classic cause of a dashboard that
|
||||||
|
renders but can't load any data.
|
||||||
|
- The compose file **fails fast** if `PRIVATE_SERVER_KEY` (or `BETTER_AUTH_SECRET`) is empty.
|
||||||
|
See [Environment & secrets](/self-hosted/configuration/secrets).
|
||||||
|
|
||||||
|
## The jobs worker and your RAM
|
||||||
|
|
||||||
|
The jobs worker is why the [server requirements](/self-hosted#server-requirements) recommend 8 GB.
|
||||||
|
|
||||||
|
It processes queues for media (probe → thumbnail → audio extraction), transcription (Whisper),
|
||||||
|
emails, notifications, AI course generation, and periodic maintenance (including reaping stuck
|
||||||
|
jobs). Video work runs **ffmpeg as a subprocess**, streaming files via disk rather than holding
|
||||||
|
whole videos in memory, but transcodes still spike RAM well above the stack's idle usage. A server
|
||||||
|
that looks comfortable at idle can OOM-kill Postgres the first time someone uploads a lecture
|
||||||
|
video; see [Troubleshooting → OOM kills](/self-hosted/troubleshooting#containers-are-oom-killed).
|
||||||
|
|
||||||
|
The worker is required, not optional: without it (or without Redis), uploads queue forever at
|
||||||
|
"processing" and most emails never send
|
||||||
|
([details](/self-hosted/troubleshooting#uploads-stuck-at-processing)). It also scales
|
||||||
|
independently: the image runs all workers in one process by default, but you can override the
|
||||||
|
container command to run, say, media processing on separate hardware from email sending.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
title: Backups
|
||||||
|
description: Back up and restore a self-hosted ClassroomIO — Postgres dumps and object-storage archives, automated or manual.
|
||||||
|
---
|
||||||
|
|
||||||
|
Two things hold all of your data: the **Postgres database** (courses, users, organizations) and
|
||||||
|
**object storage** (uploaded videos, documents, media). Back up both.
|
||||||
|
|
||||||
|
## With the script
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
./classroomio.sh backup
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes two timestamped files into `./backups/`:
|
||||||
|
|
||||||
|
- `classroomio-db-<timestamp>.sql.gz`: a full `pg_dump` of the database.
|
||||||
|
- `classroomio-minio-<timestamp>.tar.gz`: an archive of the bundled MinIO volume.
|
||||||
|
|
||||||
|
If you use external S3/R2 instead of the bundled MinIO, the second step is skipped. Use your
|
||||||
|
provider's backup or replication tools for the buckets.
|
||||||
|
|
||||||
|
`./classroomio.sh upgrade` runs this backup automatically before pulling new images, so every
|
||||||
|
upgrade has a restore point.
|
||||||
|
|
||||||
|
:::warning[Copy backups off the server]
|
||||||
|
A dead disk takes `./backups/` with it. Sync the files to another machine, an object-storage
|
||||||
|
bucket, or your provider's snapshot system on a schedule, e.g. a nightly cron:
|
||||||
|
|
||||||
|
```zsh title="crontab"
|
||||||
|
0 3 * * * cd /path/to/classroomio && ./classroomio.sh backup && rsync -a backups/ user@other-host:classroomio-backups/
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Manual commands
|
||||||
|
|
||||||
|
The script runs the equivalent of:
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
# Postgres (adjust user/db if you changed POSTGRES_USER / POSTGRES_DB)
|
||||||
|
docker compose --env-file .env -p classroomio -f docker-compose.images.yaml \
|
||||||
|
exec -T postgres pg_dump -U postgres classroomio | gzip > backups/db.sql.gz
|
||||||
|
|
||||||
|
# MinIO volume
|
||||||
|
docker run --rm -v classroomio_minio-data:/data:ro -v "$PWD/backups":/backup \
|
||||||
|
alpine tar czf /backup/minio.tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Restoring
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step>
|
||||||
|
### Stop the app services
|
||||||
|
|
||||||
|
Keep Postgres running, stop everything that writes to it:
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
docker compose --env-file .env -p classroomio -f docker-compose.images.yaml stop api dashboard jobs
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Restore the database
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
gunzip -c backups/classroomio-db-<timestamp>.sql.gz | \
|
||||||
|
docker compose --env-file .env -p classroomio -f docker-compose.images.yaml \
|
||||||
|
exec -T postgres psql -U postgres classroomio
|
||||||
|
```
|
||||||
|
|
||||||
|
Restoring into a non-empty database can conflict with existing rows. For a clean restore, drop and
|
||||||
|
recreate the database first (`dropdb`/`createdb` inside the postgres container), then run the
|
||||||
|
command above.
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Restore object storage
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
docker run --rm -v classroomio_minio-data:/data -v "$PWD/backups":/backup \
|
||||||
|
alpine sh -c "cd /data && tar xzf /backup/classroomio-minio-<timestamp>.tar.gz"
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Start everything again
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
./classroomio.sh start
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
:::danger[The fastest way to lose data is a cleanup command]
|
||||||
|
`docker compose down -v` and `docker volume prune` **delete** the `postgres-data`, `redis-data`,
|
||||||
|
and `minio-data` volumes. Never run them on a production host. See
|
||||||
|
[Troubleshooting → disk space](/self-hosted/troubleshooting#no-space-left-on-device) for safe
|
||||||
|
alternatives.
|
||||||
|
:::
|
||||||
@@ -4,8 +4,8 @@ description: Allow the external CDN, font, media, and frame domains your self-ho
|
|||||||
---
|
---
|
||||||
|
|
||||||
When `PUBLIC_IS_SELFHOSTED=true`, the dashboard ships a strict Content Security Policy that allows only
|
When `PUBLIC_IS_SELFHOSTED=true`, the dashboard ships a strict Content Security Policy that allows only
|
||||||
`'self'`. If you load assets from external domains — a CDN, custom fonts, embedded video, an external
|
`'self'`. If you load assets from external domains (a CDN, custom fonts, embedded video, an external
|
||||||
media/storage host — you must add those domains, or the browser blocks them.
|
media/storage host), you must add those domains, or the browser blocks them.
|
||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
|
|
||||||
@@ -29,13 +29,13 @@ ALLOWED_EXTERNAL_DOMAINS=https://fonts.googleapis.com,https://storage.yourdomain
|
|||||||
CSP_MEDIA_SRC_DOMAINS=https://storage.yourdomain.com
|
CSP_MEDIA_SRC_DOMAINS=https://storage.yourdomain.com
|
||||||
```
|
```
|
||||||
|
|
||||||
:::note[Runtime — no rebuild needed]
|
:::note[Runtime: no rebuild needed]
|
||||||
CSP domain variables are read at **container startup**. Change them and restart the container — you do
|
CSP domain variables are read at **container startup**. Change them and restart the container. You do
|
||||||
not need to rebuild any image.
|
not need to rebuild any image.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::warning[You don't need to add the API]
|
:::warning[You don't need to add the API]
|
||||||
Normal dashboard traffic is proxied to the API **same-origin**, so the API host does not need to be in
|
Normal dashboard traffic is proxied to the API **same-origin**, so the API host does not need to be in
|
||||||
the CSP. Add external hosts only — your CDN, video provider, font host, and object-storage/media domain
|
the CSP. Add external hosts only: your CDN, video provider, font host, and object-storage/media domain
|
||||||
(the most common one, so browsers can load uploaded media).
|
(the most common one, so browsers can load uploaded media).
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Email (SMTP)
|
|||||||
description: Configure SMTP so ClassroomIO can send signup verification, password reset, and invite emails.
|
description: Configure SMTP so ClassroomIO can send signup verification, password reset, and invite emails.
|
||||||
---
|
---
|
||||||
|
|
||||||
ClassroomIO sends transactional email — **signup verification, password resets, and invite emails** —
|
ClassroomIO sends transactional email (**signup verification, password resets, and invite emails**)
|
||||||
over SMTP. Without it the app still runs, but it can't send any of those messages, so have mail
|
over SMTP. Without it the app still runs, but it can't send any of those messages, so have mail
|
||||||
credentials ready before you deploy.
|
credentials ready before you deploy.
|
||||||
|
|
||||||
@@ -33,6 +33,6 @@ whole compose stack. The jobs worker won't send invites or notifications if SMTP
|
|||||||
|
|
||||||
## Providers
|
## Providers
|
||||||
|
|
||||||
Any standard SMTP provider works — including transactional services such as **Zoho / ZeptoMail**,
|
Any standard SMTP provider works, including transactional services such as **Zoho / ZeptoMail**,
|
||||||
Postmark, Resend (SMTP), Amazon SES, or Mailgun. See the
|
Postmark, Resend (SMTP), Amazon SES, or Mailgun. See the
|
||||||
[Nodemailer SMTP docs](https://nodemailer.com/smtp/) for provider-specific setup and port/TLS options.
|
[Nodemailer SMTP docs](https://nodemailer.com/smtp/) for provider-specific setup and port/TLS options.
|
||||||
|
|||||||
@@ -3,20 +3,21 @@ title: Configuration
|
|||||||
description: Shared configuration reference for self-hosting ClassroomIO — secrets, email, storage, CSP, integrations, licensing, and versions.
|
description: Shared configuration reference for self-hosting ClassroomIO — secrets, email, storage, CSP, integrations, licensing, and versions.
|
||||||
---
|
---
|
||||||
|
|
||||||
However you deploy ClassroomIO — [Docker](/self-hosted/docker), [Railway](/self-hosted/railway), or
|
However you deploy ClassroomIO ([Docker](/self-hosted/docker), [DigitalOcean](/self-hosted/digitalocean),
|
||||||
[Coolify](/self-hosted/coolify) — the stack reads the **same set of environment variables**. This
|
[Railway](/self-hosted/railway), [Coolify](/self-hosted/coolify), or [Dokploy](/self-hosted/dokploy)),
|
||||||
section is the single source of truth for what each group of variables does. The provider guides show
|
the stack reads the **same set of environment variables**. This section is the single source of truth
|
||||||
you *where* to set them for that platform; these pages explain *what* to set and *why*.
|
for what each group of variables does. The provider guides show you *where* to set them for that
|
||||||
|
platform; these pages explain *what* to set and *why*.
|
||||||
|
|
||||||
## How to read these pages
|
## How to read these pages
|
||||||
|
|
||||||
ClassroomIO is configured entirely through environment variables. On Docker they live in one root
|
ClassroomIO is configured entirely through environment variables. On Docker they live in one root
|
||||||
`.env` file that every `docker compose` command reads via `--env-file .env`; on Railway and Coolify you
|
`.env` file that every `docker compose` command reads via `--env-file .env`; on PaaS platforms
|
||||||
set the same variables on each service.
|
(Railway, Coolify, Dokploy) you set the same variables on each service.
|
||||||
|
|
||||||
:::note[Value = required, blank = optional]
|
:::note[Value = required, blank = optional]
|
||||||
Throughout these pages and the provider guides, a variable shown **with a value** is one you should
|
Throughout these pages and the provider guides, a variable shown **with a value** is one you should
|
||||||
set for a working deployment. A variable left **blank after `=`** is optional — fill it in only if you
|
set for a working deployment. A variable left **blank after `=`** is optional: fill it in only if you
|
||||||
use that feature. Leaving an optional variable blank never causes a build or boot error.
|
use that feature. Leaving an optional variable blank never causes a build or boot error.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ use that feature. Leaving an optional variable blank never causes a build or boo
|
|||||||
|
|
||||||
<CardGroup cols={2}>
|
<CardGroup cols={2}>
|
||||||
<Card title="Environment & secrets" href="/self-hosted/configuration/secrets" icon="key-round">
|
<Card title="Environment & secrets" href="/self-hosted/configuration/secrets" icon="key-round">
|
||||||
Generate `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` — the stack won't boot without them.
|
Generate `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY`: the stack won't boot without them.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Email (SMTP)" href="/self-hosted/configuration/email" icon="mail">
|
<Card title="Email (SMTP)" href="/self-hosted/configuration/email" icon="mail">
|
||||||
SMTP for signup verification, password resets, and invite emails.
|
SMTP for signup verification, password resets, and invite emails.
|
||||||
@@ -38,7 +39,7 @@ use that feature. Leaving an optional variable blank never causes a build or boo
|
|||||||
<Card title="Integrations" href="/self-hosted/configuration/integrations" icon="plug">
|
<Card title="Integrations" href="/self-hosted/configuration/integrations" icon="plug">
|
||||||
Google OAuth, Unsplash, OpenAI transcription and AI course generation.
|
Google OAuth, Unsplash, OpenAI transcription and AI course generation.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="SSO & licensing" href="/self-hosted/configuration/sso" icon="id-card">
|
<Card title="Enterprise & licensing" href="/self-hosted/enterprise" icon="id-card">
|
||||||
Set `LICENSE_KEY` to unlock Enterprise features such as SSO.
|
Set `LICENSE_KEY` to unlock Enterprise features such as SSO.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Versions & upgrades" href="/self-hosted/configuration/versions" icon="git-branch">
|
<Card title="Versions & upgrades" href="/self-hosted/configuration/versions" icon="git-branch">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Integrations
|
|||||||
description: Optional integrations for self-hosted ClassroomIO — Google OAuth, Unsplash, OpenAI, and branding.
|
description: Optional integrations for self-hosted ClassroomIO — Google OAuth, Unsplash, OpenAI, and branding.
|
||||||
---
|
---
|
||||||
|
|
||||||
These integrations are all **optional** — ClassroomIO runs without any of them. Add a variable only if
|
These integrations are all **optional**: ClassroomIO runs without any of them. Add a variable only if
|
||||||
you use that feature; leaving it blank never causes a build or boot error.
|
you use that feature; leaving it blank never causes a build or boot error.
|
||||||
|
|
||||||
## Google OAuth
|
## Google OAuth
|
||||||
@@ -43,6 +43,6 @@ Enables the in-app Unsplash image search.
|
|||||||
| `PRIVATE_APP_SUBDOMAINS` | Subdomains served by the dashboard |
|
| `PRIVATE_APP_SUBDOMAINS` | Subdomains served by the dashboard |
|
||||||
|
|
||||||
:::note[Enterprise features]
|
:::note[Enterprise features]
|
||||||
`LICENSE_KEY` unlocks Enterprise-only integrations such as SSO — see
|
`LICENSE_KEY` unlocks Enterprise-only integrations such as SSO. See
|
||||||
[SSO & licensing](/self-hosted/configuration/sso).
|
[Enterprise & licensing](/self-hosted/enterprise).
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -16,23 +16,23 @@ openssl rand -hex 32 # PRIVATE_SERVER_KEY (must be identical in the api and
|
|||||||
| Variable | What it's for | Where it goes |
|
| Variable | What it's for | Where it goes |
|
||||||
|----------|---------------|---------------|
|
|----------|---------------|---------------|
|
||||||
| `BETTER_AUTH_SECRET` | Signs authentication sessions. | The **api** service. |
|
| `BETTER_AUTH_SECRET` | Signs authentication sessions. | The **api** service. |
|
||||||
| `PRIVATE_SERVER_KEY` | Authenticates server-side dashboard → API requests. | **Both** the api and dashboard — the value must be **identical** in each. |
|
| `PRIVATE_SERVER_KEY` | Authenticates server-side dashboard → API requests. | **Both** the api and dashboard; the value must be **identical** in each. |
|
||||||
|
|
||||||
:::danger[The stack won't start without them]
|
:::danger[The stack won't start without them]
|
||||||
The compose file **fails fast** if `BETTER_AUTH_SECRET` or `PRIVATE_SERVER_KEY` is empty — the
|
The compose file **fails fast** if `BETTER_AUTH_SECRET` or `PRIVATE_SERVER_KEY` is empty: the
|
||||||
containers refuse to boot rather than run with a predictable, guessable session-signing secret. Set
|
containers refuse to boot rather than run with a predictable, guessable session-signing secret. Set
|
||||||
both to a strong random value (e.g. `openssl rand -hex 32`).
|
both to a strong random value (e.g. `openssl rand -hex 32`).
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::note[Who generates them]
|
:::note[Who generates them]
|
||||||
Only the source-build path's `run-docker-full-stack.sh` auto-generates these when missing. The
|
On Docker, `./classroomio.sh` auto-generates both secrets when they're missing (on install and
|
||||||
pre-built image path, Railway, and Coolify do **not** run that script — you generate the secrets
|
start, for the pull and build paths alike). Railway, Coolify, and Dokploy do **not** run that
|
||||||
yourself and paste them in.
|
script. There you generate the secrets yourself and paste them in.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Database credentials
|
## Database credentials
|
||||||
|
|
||||||
Postgres runs inside the stack (or as a managed plugin on Railway). Set a strong password — the
|
Postgres runs inside the stack (or as a managed plugin on Railway). Set a strong password. The
|
||||||
defaults are for local use only.
|
defaults are for local use only.
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
@@ -41,6 +41,26 @@ defaults are for local use only.
|
|||||||
| `POSTGRES_USER` | Database user | `postgres` |
|
| `POSTGRES_USER` | Database user | `postgres` |
|
||||||
| `POSTGRES_PASSWORD` | Database password — set a **strong** value in production | `postgres` |
|
| `POSTGRES_PASSWORD` | Database password — set a **strong** value in production | `postgres` |
|
||||||
|
|
||||||
|
### Rotating `POSTGRES_PASSWORD` after install
|
||||||
|
|
||||||
|
:::danger[Editing `.env` alone doesn't rotate it]
|
||||||
|
The official Postgres image only reads `POSTGRES_PASSWORD` when it initializes a **fresh,
|
||||||
|
empty** data directory. Once `./classroomio.sh install` has run once, the data volume
|
||||||
|
already exists — changing `POSTGRES_PASSWORD` in `.env` afterward and restarting does
|
||||||
|
**not** change the database's actual password. It only changes the password `api`/`jobs`
|
||||||
|
*attempt* to connect with, so a mismatch breaks every DB connection.
|
||||||
|
|
||||||
|
To actually rotate it after install:
|
||||||
|
|
||||||
|
1. Set the new password inside Postgres itself:
|
||||||
|
```zsh title="Terminal"
|
||||||
|
docker compose --env-file .env -p classroomio -f docker-compose.images.yaml \
|
||||||
|
exec postgres psql -U postgres -c "ALTER ROLE postgres WITH PASSWORD '<new-password>';"
|
||||||
|
```
|
||||||
|
2. Update `POSTGRES_PASSWORD` in `.env` to the same value.
|
||||||
|
3. Run `./classroomio.sh restart` to recreate `api`/`jobs`/`postgres` with the matching value.
|
||||||
|
:::
|
||||||
|
|
||||||
## Mode & routing basics
|
## Mode & routing basics
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
@@ -50,7 +70,7 @@ defaults are for local use only.
|
|||||||
| `DASHBOARD_ORIGIN` | Public dashboard origin, used as the auth base URL and in invite/email links | `http://localhost:3082` |
|
| `DASHBOARD_ORIGIN` | Public dashboard origin, used as the auth base URL and in invite/email links | `http://localhost:3082` |
|
||||||
|
|
||||||
:::warning[Use an `https://` origin in production]
|
:::warning[Use an `https://` origin in production]
|
||||||
`DASHBOARD_ORIGIN` (and `ORIGIN` on the dashboard) must be the real `https://` URL your users visit —
|
`DASHBOARD_ORIGIN` (and `ORIGIN` on the dashboard) must be the real `https://` URL your users visit:
|
||||||
auth session cookies are `Secure`, so login silently fails over plain HTTP on any public domain. See
|
auth session cookies are `Secure`, so login silently fails over plain HTTP on any public domain. See
|
||||||
[Self Hosting → HTTPS is required in production](/self-hosted#https-is-required-in-production).
|
[Self Hosting → HTTPS is required in production](/self-hosted#https-is-required-in-production).
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
title: SSO & licensing
|
|
||||||
description: Set LICENSE_KEY to unlock Enterprise features such as SSO on your self-hosted ClassroomIO.
|
|
||||||
---
|
|
||||||
|
|
||||||
Enterprise-only features — **SSO**, **token auth**, and no-tracking mode — require an Enterprise
|
|
||||||
**license key**. If you don't use any Enterprise feature, you don't need a key; leave `LICENSE_KEY`
|
|
||||||
unset and everything else works.
|
|
||||||
|
|
||||||
## Setting `LICENSE_KEY`
|
|
||||||
|
|
||||||
`LICENSE_KEY` is read by the **api** service. Set it wherever that service takes its environment:
|
|
||||||
|
|
||||||
| Deployment | Where to set it |
|
|
||||||
|------------|-----------------|
|
|
||||||
| Local development | `apps/api/.env` |
|
|
||||||
| Docker Compose | repository root `.env` |
|
|
||||||
| Railway | the `cio-api` service environment |
|
|
||||||
| Coolify | the resource's environment variables |
|
|
||||||
|
|
||||||
```zsh title=".env"
|
|
||||||
LICENSE_KEY=<your enterprise license key>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Getting a license
|
|
||||||
|
|
||||||
[Book a call](http://cal.com/classroomio/enterprise) to get an Enterprise license key.
|
|
||||||
|
|
||||||
## Feature docs
|
|
||||||
|
|
||||||
Once the key is set, configure the features themselves:
|
|
||||||
|
|
||||||
- [Enterprise SSO](/enterprise-sso) — Single Sign-On with OIDC (Okta, Azure AD, and more).
|
|
||||||
- [Token Auth](/token-auth) — issue signed tokens to log users in from your own app.
|
|
||||||
@@ -7,7 +7,7 @@ ClassroomIO stores uploaded files and videos in **object storage**. You have two
|
|||||||
**bundled MinIO** alongside the stack, or point ClassroomIO at an **external S3-compatible** service
|
**bundled MinIO** alongside the stack, or point ClassroomIO at an **external S3-compatible** service
|
||||||
(AWS S3, Cloudflare R2, etc.). Both speak the S3 API and use the same `OBJECT_STORAGE_*` variables.
|
(AWS S3, Cloudflare R2, etc.). Both speak the S3 API and use the same `OBJECT_STORAGE_*` variables.
|
||||||
|
|
||||||
## Option 1 — Bundled MinIO (default)
|
## Option 1: Bundled MinIO (default)
|
||||||
|
|
||||||
The Docker and Coolify compose files include **MinIO** under the `minio` profile, and the startup
|
The Docker and Coolify compose files include **MinIO** under the `minio` profile, and the startup
|
||||||
script auto-configures it. You only change these for custom credentials.
|
script auto-configures it. You only change these for custom credentials.
|
||||||
@@ -23,17 +23,26 @@ script auto-configures it. You only change these for custom credentials.
|
|||||||
| `OBJECT_STORAGE_FORCE_PATH_STYLE` | `true` (required for MinIO) |
|
| `OBJECT_STORAGE_FORCE_PATH_STYLE` | `true` (required for MinIO) |
|
||||||
| `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` | `http://localhost:9000/media` |
|
| `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` | `http://localhost:9000/media` |
|
||||||
|
|
||||||
:::warning[MinIO is your durability boundary]
|
:::note[Changing your domain later]
|
||||||
Single-node MinIO has no redundancy — its volume is the only thing protecting your uploads. Give it
|
If you change `DASHBOARD_ORIGIN` after Install, `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` is
|
||||||
**persistent storage** and **back it up**. If you'd rather not operate storage yourself, use external
|
re-derived from it automatically the next time you run `./classroomio.sh restart` (or
|
||||||
**Cloudflare R2 / AWS S3** instead (R2 has no egress fees) — see below.
|
`start`/`upgrade`). If you've bundled MinIO but manually pinned
|
||||||
|
`OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` to a custom media CDN, note that a domain change will
|
||||||
|
overwrite it back to `<DASHBOARD_ORIGIN>/media` — re-set it afterward if you need the custom
|
||||||
|
value.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Option 2 — External S3 / Cloudflare R2
|
:::warning[MinIO is your durability boundary]
|
||||||
|
Single-node MinIO has no redundancy: its volume is the only thing protecting your uploads. Give it
|
||||||
|
**persistent storage** and **back it up**. If you'd rather not operate storage yourself, use external
|
||||||
|
**Cloudflare R2 / AWS S3** instead (R2 has no egress fees); see below.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Option 2: External S3 / Cloudflare R2
|
||||||
|
|
||||||
The `OBJECT_STORAGE_*` variables speak the S3 API, so they work with **any S3-compatible provider**,
|
The `OBJECT_STORAGE_*` variables speak the S3 API, so they work with **any S3-compatible provider**,
|
||||||
including Cloudflare R2 and AWS S3. Point them at the provider and start **without** the bundled MinIO
|
including Cloudflare R2 and AWS S3. Point them at the provider and start **without** the bundled MinIO
|
||||||
(`./run-docker-full-stack.sh --no-minio`, or leave the `minio` profile off).
|
(`./classroomio.sh start --no-minio`, or leave the `minio` profile off).
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
@@ -48,7 +57,7 @@ including Cloudflare R2 and AWS S3. Point them at the provider and start **witho
|
|||||||
:::note[Legacy `CLOUDFLARE_*` variables]
|
:::note[Legacy `CLOUDFLARE_*` variables]
|
||||||
ClassroomIO also accepts a legacy Cloudflare R2 config (`CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_ACCESS_KEY`,
|
ClassroomIO also accepts a legacy Cloudflare R2 config (`CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_ACCESS_KEY`,
|
||||||
`CLOUDFLARE_SECRET_ACCESS_KEY`, …). It is a **fallback** used only when the `OBJECT_STORAGE_*` variables
|
`CLOUDFLARE_SECRET_ACCESS_KEY`, …). It is a **fallback** used only when the `OBJECT_STORAGE_*` variables
|
||||||
are absent — prefer `OBJECT_STORAGE_*` for new deployments (it covers R2 too). For step-by-step R2 bucket
|
are absent; prefer `OBJECT_STORAGE_*` for new deployments (it covers R2 too). For step-by-step R2 bucket
|
||||||
and CORS setup, see the [Cloudflare Setup](/guides/contributing/cloudflare-setup) guide.
|
and CORS setup, see the [Cloudflare Setup](/guides/contributing/cloudflare-setup) guide.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ ClassroomIO publishes versioned images. `CIO_VERSION` controls which tag you run
|
|||||||
|
|
||||||
- **Pin an exact release** (e.g. `1.0.0`) in production, so a merge to `main` never restarts you onto
|
- **Pin an exact release** (e.g. `1.0.0`) in production, so a merge to `main` never restarts you onto
|
||||||
changed images unattended.
|
changed images unattended.
|
||||||
- `latest` is the freshest `main` build — it moves on **every merge** (rolling; may include unreleased
|
- `latest` is the freshest `main` build: it moves on **every merge** (rolling; may include unreleased
|
||||||
code). The version tags (`1.0.0`, `1.0`, `1`) are the immutable, pinned releases.
|
code). The version tags (`1.0.0`, `1.0`, `1`) are the immutable, pinned releases.
|
||||||
|
|
||||||
```zsh title=".env"
|
```zsh title=".env"
|
||||||
@@ -25,7 +25,13 @@ Before deploying, confirm the tag you set actually exists at
|
|||||||
|
|
||||||
### Pre-built images (Docker)
|
### Pre-built images (Docker)
|
||||||
|
|
||||||
Bump `CIO_VERSION` in `.env`, then re-pull and recreate:
|
```zsh title="Terminal"
|
||||||
|
./classroomio.sh upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
The script [backs up](/self-hosted/backups) Postgres and MinIO first, lets you pin a new
|
||||||
|
`CIO_VERSION`, then re-pulls and recreates. Running compose by hand is equivalent (minus the
|
||||||
|
backup): bump `CIO_VERSION` in `.env`, then:
|
||||||
|
|
||||||
```zsh title="Terminal"
|
```zsh title="Terminal"
|
||||||
docker compose -f docker-compose.images.yaml --env-file .env --profile minio pull
|
docker compose -f docker-compose.images.yaml --env-file .env --profile minio pull
|
||||||
@@ -34,12 +40,15 @@ docker compose -f docker-compose.images.yaml --env-file .env --profile minio up
|
|||||||
|
|
||||||
### Build from source (Docker)
|
### Build from source (Docker)
|
||||||
|
|
||||||
`git pull`, then re-run `./run-docker-full-stack.sh --build` to rebuild.
|
`git pull`, then re-run `./classroomio.sh upgrade --build` to rebuild. Like the pre-built path,
|
||||||
|
this backs up Postgres and MinIO first, then rebuilds and restarts. (`start --build` also
|
||||||
|
rebuilds, but skips the backup step — use `upgrade --build` when upgrading an existing
|
||||||
|
deployment with real data.)
|
||||||
|
|
||||||
### Railway / Coolify
|
### Railway / Coolify / Dokploy / DigitalOcean
|
||||||
|
|
||||||
Set `CIO_VERSION` to the newer release and redeploy. Because you pin a version, a new release never
|
Set `CIO_VERSION` to the newer release and redeploy (on a DigitalOcean-style VPS: `dc pull && dc up -d`).
|
||||||
restarts you onto changed images unattended.
|
Because you pin a version, a new release never restarts you onto changed images unattended.
|
||||||
|
|
||||||
## Rolling back
|
## Rolling back
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Coolify
|
|||||||
description: Deploy ClassroomIO on Coolify using the pull-based Docker Compose stack, with a video walkthrough.
|
description: Deploy ClassroomIO on Coolify using the pull-based Docker Compose stack, with a video walkthrough.
|
||||||
---
|
---
|
||||||
|
|
||||||
[Coolify](https://coolify.io) is a self-hosted PaaS (an open-source Heroku/Netlify alternative) you run on your own VPS. It runs ClassroomIO from the published `classroomio/*` images — your Coolify host **pulls** them and never builds anything.
|
[Coolify](https://coolify.io) is a self-hosted PaaS (an open-source Heroku/Netlify alternative) you run on your own VPS. It runs ClassroomIO from the published `classroomio/*` images: your Coolify host **pulls** them and never builds anything.
|
||||||
|
|
||||||
## Watch the walkthrough
|
## Watch the walkthrough
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ This uses a **Docker Compose resource**: you paste the compose file and set ever
|
|||||||
<Step>
|
<Step>
|
||||||
### Create a project and add a Docker Compose resource
|
### Create a project and add a Docker Compose resource
|
||||||
|
|
||||||
Log in to Coolify and **create a project**. When you add a resource, Coolify asks what type it is — GitHub, a Docker image, a service, or a **Docker Compose** file. Choose **Docker Compose**, then **paste the contents of [`docker-compose.images.yaml`](https://github.com/classroomio/classroomio/blob/main/docker-compose.images.yaml)** into the editor.
|
Log in to Coolify and **create a project**. When you add a resource, Coolify asks what type it is: GitHub, a Docker image, a service, or a **Docker Compose** file. Choose **Docker Compose**, then **paste the contents of [`docker-compose.images.yaml`](https://github.com/classroomio/classroomio/blob/main/docker-compose.images.yaml)** into the editor.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -74,13 +74,13 @@ SMTP_SENDER=ClassroomIO <[email protected]>
|
|||||||
# Object storage + domains: see the full reference below
|
# Object storage + domains: see the full reference below
|
||||||
```
|
```
|
||||||
|
|
||||||
👉 The essentials above get you started, but the complete list of what to set — object storage, domains, and every optional variable — is in the [**Environment variable reference**](#environment-variable-reference) below. Copy that full block before you deploy.
|
The essentials above get you started, but the complete list of what to set (object storage, domains, and every optional variable) is in the [**Environment variable reference**](#environment-variable-reference) below. Copy that full block before you deploy.
|
||||||
|
|
||||||
:::note[How to read the reference: value = required, blank = optional]
|
:::note[How to read the reference: value = required, blank = optional]
|
||||||
There's no way to guess which variables Coolify needs, so the [Environment variable
|
There's no way to guess which variables Coolify needs, so the [Environment variable
|
||||||
reference](#environment-variable-reference) spells it out. The rule is simple: **every variable
|
reference](#environment-variable-reference) spells it out. The rule is simple: **every variable
|
||||||
shown with a value must be set** for a smooth deployment (secrets, database, domains, object
|
shown with a value must be set** for a smooth deployment (secrets, database, domains, object
|
||||||
storage, and SMTP are all needed). **Every variable left blank after `=` is optional** — fill it in
|
storage, and SMTP are all needed). **Every variable left blank after `=` is optional**: fill it in
|
||||||
only if you use that integration. Leaving an optional one blank never causes a build or boot error.
|
only if you use that integration. Leaving an optional one blank never causes a build or boot error.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ only if you use that integration. Leaving an optional one blank never causes a b
|
|||||||
`CIO_VERSION` chooses the tag pulled for `classroomio/{api,dashboard,jobs}`. **Confirm the tag you use actually exists** at [hub.docker.com/r/classroomio](https://hub.docker.com/r/classroomio/) before deploying.
|
`CIO_VERSION` chooses the tag pulled for `classroomio/{api,dashboard,jobs}`. **Confirm the tag you use actually exists** at [hub.docker.com/r/classroomio](https://hub.docker.com/r/classroomio/) before deploying.
|
||||||
|
|
||||||
:::note[Pin a released version]
|
:::note[Pin a released version]
|
||||||
Set `CIO_VERSION=1.0.0` to pin an exact release. `latest` is the freshest `main` build — it moves
|
Set `CIO_VERSION=1.0.0` to pin an exact release. `latest` is the freshest `main` build: it moves
|
||||||
on every merge (rolling; may include unreleased code), while the version tags (`1.0.0`, `1.0`, `1`)
|
on every merge (rolling; may include unreleased code), while the version tags (`1.0.0`, `1.0`, `1`)
|
||||||
are immutable, pinned releases. Pinning an exact release makes upgrades deliberate and deploys
|
are immutable, pinned releases. Pinning an exact release makes upgrades deliberate and deploys
|
||||||
reproducible.
|
reproducible.
|
||||||
@@ -107,7 +107,7 @@ reproducible.
|
|||||||
|
|
||||||
Open the **dashboard** service → **Settings** and set its domain, including the internal port, e.g. `https://dash.yourdomain.com:3082`. Coolify maps that FQDN to the container's port `3082` and issues the certificate through its proxy.
|
Open the **dashboard** service → **Settings** and set its domain, including the internal port, e.g. `https://dash.yourdomain.com:3082`. Coolify maps that FQDN to the container's port `3082` and issues the certificate through its proxy.
|
||||||
|
|
||||||
The dashboard proxies API calls server-side, so you normally only expose the dashboard publicly — the `api` service stays internal.
|
The dashboard proxies API calls server-side, so you normally only expose the dashboard publicly. The `api` service stays internal.
|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
@@ -115,7 +115,7 @@ The dashboard proxies API calls server-side, so you normally only expose the das
|
|||||||
<Step>
|
<Step>
|
||||||
### Deploy
|
### Deploy
|
||||||
|
|
||||||
Click **Deploy**. Coolify pulls `classroomio/{api,dashboard,jobs}:${CIO_VERSION}`, starts Postgres + Redis, the API container runs database setup on first boot, then the dashboard and the **jobs worker** come up. Watch the **logs** — they tell you when every service is up.
|
Click **Deploy**. Coolify pulls `classroomio/{api,dashboard,jobs}:${CIO_VERSION}`, starts Postgres + Redis, the API container runs database setup on first boot, then the dashboard and the **jobs worker** come up. Watch the **logs**: they tell you when every service is up.
|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
@@ -123,7 +123,7 @@ Click **Deploy**. Coolify pulls `classroomio/{api,dashboard,jobs}:${CIO_VERSION}
|
|||||||
<Step>
|
<Step>
|
||||||
### Open the app and sign up
|
### Open the app and sign up
|
||||||
|
|
||||||
Once the logs show the stack is healthy, open your dashboard URL (`https://dash.yourdomain.com`) in a new browser tab and **sign up**. The **first** account becomes the organization **admin**. Every signup or sign-in after that joins as a student — unless you invite them with a specific role.
|
Once the logs show the stack is healthy, open your dashboard URL (`https://dash.yourdomain.com`) in a new browser tab and **sign up**. The **first** account becomes the organization **admin**. Every signup or sign-in after that joins as a student, unless you invite them with a specific role.
|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
@@ -132,11 +132,11 @@ Once the logs show the stack is healthy, open your dashboard URL (`https://dash.
|
|||||||
|
|
||||||
## Environment variable reference
|
## Environment variable reference
|
||||||
|
|
||||||
You set these on the resource's Environment Variables page (Step 3). This is the complete picture — copy the block below, then replace the placeholder values with your own.
|
You set these on the resource's Environment Variables page (Step 3). This is the complete picture: copy the block below, then replace the placeholder values with your own.
|
||||||
|
|
||||||
:::note[Value = required, blank = optional]
|
:::note[Value = required, blank = optional]
|
||||||
**Every variable shown with a value below is required** for a working deployment. **Every variable
|
**Every variable shown with a value below is required** for a working deployment. **Every variable
|
||||||
left blank after `=` is optional** — fill it in only if you use that feature. A blank optional
|
left blank after `=` is optional**: fill it in only if you use that feature. A blank optional
|
||||||
variable never causes a build or boot error, so don't put junk into fields you aren't using.
|
variable never causes a build or boot error, so don't put junk into fields you aren't using.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ SMTP_PASSWORD=<smtp password>
|
|||||||
SMTP_SENDER=ClassroomIO <[email protected]>
|
SMTP_SENDER=ClassroomIO <[email protected]>
|
||||||
|
|
||||||
# ── Optional — leave blank unless you need them ──
|
# ── Optional — leave blank unless you need them ──
|
||||||
CIO_VERSION=1.0.0 # pinned release; blank = latest (rolling main build)
|
CIO_VERSION= # pin a release, e.g. 1.0.0; blank = latest (rolling main build)
|
||||||
GOOGLE_CLIENT_ID= # Google OAuth sign-in
|
GOOGLE_CLIENT_ID= # Google OAuth sign-in
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
OPENAI_API_KEY= # transcription + AI course generation
|
OPENAI_API_KEY= # transcription + AI course generation
|
||||||
@@ -201,7 +201,7 @@ OBJECT_STORAGE_REGION= # only for some external S3 providers
|
|||||||
SKIP_DB_SETUP= # blank = run DB setup on first boot (leave blank)
|
SKIP_DB_SETUP= # blank = run DB setup on first boot (leave blank)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Required — set all of these
|
### Required: set all of these
|
||||||
|
|
||||||
| Variable | What it's for |
|
| Variable | What it's for |
|
||||||
|----------|---------------|
|
|----------|---------------|
|
||||||
@@ -229,10 +229,10 @@ SKIP_DB_SETUP= # blank = run DB setup on first boot (leave blank)
|
|||||||
Of the above, `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` are the two the compose file
|
Of the above, `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` are the two the compose file
|
||||||
**fails fast** on: leave either empty and the containers refuse to boot (so ClassroomIO never runs
|
**fails fast** on: leave either empty and the containers refuse to boot (so ClassroomIO never runs
|
||||||
with a predictable session-signing secret). The rest won't abort the boot, but the deployment
|
with a predictable session-signing secret). The rest won't abort the boot, but the deployment
|
||||||
won't work correctly without them — set them all.
|
won't work correctly without them. Set them all.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Optional — leave blank unless you need them
|
### Optional: leave blank unless you need them
|
||||||
|
|
||||||
| Variable | Enables |
|
| Variable | Enables |
|
||||||
|----------|---------|
|
|----------|---------|
|
||||||
@@ -255,7 +255,7 @@ Enable the **bundled MinIO** with `COMPOSE_PROFILES=minio` and Coolify runs it a
|
|||||||
(`OBJECT_STORAGE_PUBLIC_ENDPOINT` / `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL`) so browsers can load media.
|
(`OBJECT_STORAGE_PUBLIC_ENDPOINT` / `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL`) so browsers can load media.
|
||||||
|
|
||||||
:::warning[MinIO is your durability boundary]
|
:::warning[MinIO is your durability boundary]
|
||||||
Single-node MinIO has no redundancy — its volume is the only thing protecting your uploads, so give it
|
Single-node MinIO has no redundancy: its volume is the only thing protecting your uploads, so give it
|
||||||
persistent storage in Coolify and back it up.
|
persistent storage in Coolify and back it up.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: DigitalOcean
|
|||||||
description: Deploy ClassroomIO to a DigitalOcean droplet with Docker Compose and Caddy, including automatic HTTPS.
|
description: Deploy ClassroomIO to a DigitalOcean droplet with Docker Compose and Caddy, including automatic HTTPS.
|
||||||
---
|
---
|
||||||
|
|
||||||
Run ClassroomIO on a plain **DigitalOcean droplet** — no PaaS in between. You get the
|
Run ClassroomIO on a plain **DigitalOcean droplet**, no PaaS in between. You get the
|
||||||
[pull-based Docker Compose stack](/self-hosted/docker) plus a [Caddy](https://caddyserver.com)
|
[pull-based Docker Compose stack](/self-hosted/docker) plus a [Caddy](https://caddyserver.com)
|
||||||
reverse proxy that obtains and renews HTTPS certificates automatically.
|
reverse proxy that obtains and renews HTTPS certificates automatically.
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ AWS Lightsail**, or any Ubuntu 24.04 server you can SSH into.
|
|||||||
|
|
||||||
:::note[How this differs from the PaaS guides]
|
:::note[How this differs from the PaaS guides]
|
||||||
[Railway](/self-hosted/railway), [Coolify](/self-hosted/coolify) and [Dokploy](/self-hosted/dokploy)
|
[Railway](/self-hosted/railway), [Coolify](/self-hosted/coolify) and [Dokploy](/self-hosted/dokploy)
|
||||||
terminate HTTPS for you. On a raw VPS you provide that yourself — this page covers the reverse proxy,
|
terminate HTTPS for you. On a raw VPS you provide that yourself: this page covers the reverse proxy,
|
||||||
the certificates, and closing your internal ports off from the internet.
|
the certificates, and closing your internal ports off from the internet.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -35,12 +35,12 @@ Prefer to follow along? This video walks through the whole deployment end to end
|
|||||||
| A | `app` | your droplet's IP |
|
| A | `app` | your droplet's IP |
|
||||||
| A | `s3` | your droplet's IP |
|
| A | `s3` | your droplet's IP |
|
||||||
|
|
||||||
- An **SMTP provider** for signup verification, password resets, and invites — see
|
- An **SMTP provider** for signup verification, password resets, and invites. See
|
||||||
[Configuration → Email (SMTP)](/self-hosted/configuration/email).
|
[Configuration → Email (SMTP)](/self-hosted/configuration/email).
|
||||||
|
|
||||||
:::note[No domain? You can still get real HTTPS]
|
:::note[No domain? You can still get real HTTPS]
|
||||||
Use [sslip.io](https://sslip.io), a public wildcard-DNS service where `<anything>.<ip>.sslip.io`
|
Use [sslip.io](https://sslip.io), a public wildcard-DNS service where `<anything>.<ip>.sslip.io`
|
||||||
resolves to that IP — so no records are needed. In Step 1 set
|
resolves to that IP, so no records are needed. In Step 1 set
|
||||||
`export DOMAIN=<your-droplet-ip>.sslip.io` instead of your own domain, and **every other command on
|
`export DOMAIN=<your-droplet-ip>.sslip.io` instead of your own domain, and **every other command on
|
||||||
this page is identical**. Read the [caveat](#a-note-on-sslipio) before using it for anything real.
|
this page is identical**. Read the [caveat](#a-note-on-sslipio) before using it for anything real.
|
||||||
:::
|
:::
|
||||||
@@ -152,7 +152,7 @@ sudo usermod -aG docker cio
|
|||||||
```
|
```
|
||||||
|
|
||||||
:::warning[Use the literal username, not `$USER`]
|
:::warning[Use the literal username, not `$USER`]
|
||||||
`$USER` expands to whoever runs the shell — run it from a root session and it grants **root** rather
|
`$USER` expands to whoever runs the shell. Run it from a root session and it grants **root** rather
|
||||||
than your user, and every later Docker command fails with `permission denied` on
|
than your user, and every later Docker command fails with `permission denied` on
|
||||||
`/var/run/docker.sock`. Check with `getent group docker`.
|
`/var/run/docker.sock`. Check with `getent group docker`.
|
||||||
:::
|
:::
|
||||||
@@ -168,7 +168,7 @@ id | grep -o docker # -> docker
|
|||||||
docker compose version # must be v2.24.0 or newer
|
docker compose version # must be v2.24.0 or newer
|
||||||
```
|
```
|
||||||
|
|
||||||
Compose **v2.24.0+** is required — Step 6 uses the `!override` YAML tag. Older versions
|
Compose **v2.24.0+** is required: Step 6 uses the `!override` YAML tag. Older versions
|
||||||
*concatenate* port lists instead of replacing them, which would leave your internal services exposed
|
*concatenate* port lists instead of replacing them, which would leave your internal services exposed
|
||||||
to the internet.
|
to the internet.
|
||||||
</Step>
|
</Step>
|
||||||
@@ -181,8 +181,8 @@ mkdir -p ~/classroomio && cd ~/classroomio
|
|||||||
curl -O https://raw.githubusercontent.com/classroomio/classroomio/main/docker-compose.images.yaml
|
curl -O https://raw.githubusercontent.com/classroomio/classroomio/main/docker-compose.images.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
This is the same [pull-based stack](/self-hosted/docker#run-from-pre-built-images-recommended) used
|
This is the same [pull-based stack](/self-hosted/docker#quick-start) used
|
||||||
by the other guides — it pulls the published `classroomio/*` images and builds nothing.
|
by the other guides. It pulls the published `classroomio/*` images and builds nothing.
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step>
|
<Step>
|
||||||
@@ -242,11 +242,11 @@ Then fill in your SMTP credentials with `nano .env`.
|
|||||||
|
|
||||||
Three of these break deployments more often than all the others combined:
|
Three of these break deployments more often than all the others combined:
|
||||||
|
|
||||||
- **`OBJECT_STORAGE_PUBLIC_ENDPOINT`** — where the browser fetches video from. Left at the template
|
- **`OBJECT_STORAGE_PUBLIC_ENDPOINT`**: where the browser fetches video from. Left at the template
|
||||||
default of `localhost`, every video URL points at the *visitor's own machine*.
|
default of `localhost`, every video URL points at the *visitor's own machine*.
|
||||||
- **`DASHBOARD_ORIGIN`** — must match the browser URL exactly, `https://` included, no trailing
|
- **`DASHBOARD_ORIGIN`**: must match the browser URL exactly, `https://` included, no trailing
|
||||||
slash. A mismatch breaks login with CSRF errors.
|
slash. A mismatch breaks login with CSRF errors.
|
||||||
- **`CSP_MEDIA_SRC_DOMAINS` / `CSP_CONNECT_SRC_DOMAINS`** — because storage is on a second hostname,
|
- **`CSP_MEDIA_SRC_DOMAINS` / `CSP_CONNECT_SRC_DOMAINS`**: because storage is on a second hostname,
|
||||||
the browser blocks its media unless these allow it. Symptom: the page looks fine, images and video
|
the browser blocks its media unless these allow it. Symptom: the page looks fine, images and video
|
||||||
silently don't load, and the only clue is in the browser console.
|
silently don't load, and the only clue is in the browser console.
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ Every remaining variable is documented in [Configuration](/self-hosted/configura
|
|||||||
<Step>
|
<Step>
|
||||||
### Add Caddy and close the internal ports
|
### Add Caddy and close the internal ports
|
||||||
|
|
||||||
Right now each part of ClassroomIO runs on its own port number — the dashboard on `3082`, file
|
Right now each part of ClassroomIO runs on its own port number: the dashboard on `3082`, file
|
||||||
storage on `9000`. That leaves two problems: nobody wants to visit `yoursite.com:3082`, and neither
|
storage on `9000`. That leaves two problems: nobody wants to visit `yoursite.com:3082`, and neither
|
||||||
port is encrypted, so there would be no padlock in the browser.
|
port is encrypted, so there would be no padlock in the browser.
|
||||||
|
|
||||||
@@ -328,14 +328,14 @@ EOF
|
|||||||
```
|
```
|
||||||
|
|
||||||
Those eight lines are the entire HTTPS setup. The first time someone visits your site, Caddy asks
|
Those eight lines are the entire HTTPS setup. The first time someone visits your site, Caddy asks
|
||||||
Let's Encrypt for a free certificate, installs it, and from then on renews it before it expires —
|
Let's Encrypt for a free certificate, installs it, and from then on renews it before it expires.
|
||||||
you never touch it again.
|
You never touch it again.
|
||||||
|
|
||||||
You could use nginx instead, but you'd also be installing certbot, setting up a renewal timer, and
|
You could use nginx instead, but you'd also be installing certbot, setting up a renewal timer, and
|
||||||
hand-writing the forwarding rules that Caddy applies by default.
|
hand-writing the forwarding rules that Caddy applies by default.
|
||||||
|
|
||||||
:::warning[Your firewall does not cover Docker]
|
:::warning[Your firewall does not cover Docker]
|
||||||
You set up a firewall back in Step 3. It does **not** apply to Docker containers — Docker writes its
|
You set up a firewall back in Step 3. It does **not** apply to Docker containers: Docker writes its
|
||||||
own networking rules underneath it, so a container that publishes a port stays reachable from the
|
own networking rules underneath it, so a container that publishes a port stays reachable from the
|
||||||
internet even when `ufw` says that port is blocked.
|
internet even when `ufw` says that port is blocked.
|
||||||
|
|
||||||
@@ -367,7 +367,7 @@ dc pull
|
|||||||
|
|
||||||
:::warning[This downloads ~12 GB and takes 10–20 minutes]
|
:::warning[This downloads ~12 GB and takes 10–20 minutes]
|
||||||
The images are large (`api` 3.7 GB, `dashboard` 3.9 GB, `jobs` 4.2 GB). On a 2-vCPU droplet the
|
The images are large (`api` 3.7 GB, `dashboard` 3.9 GB, `jobs` 4.2 GB). On a 2-vCPU droplet the
|
||||||
**progress bars sit still for minutes at a time — this is normal, it is not frozen.** Don't cancel it.
|
**progress bars sit still for minutes at a time. This is normal, it is not frozen.** Don't cancel it.
|
||||||
:::
|
:::
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
@@ -378,7 +378,7 @@ The images are large (`api` 3.7 GB, `dashboard` 3.9 GB, `jobs` 4.2 GB). On a 2-v
|
|||||||
dc up -d
|
dc up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Services start in dependency order — Postgres and Redis must report healthy before the API starts,
|
Services start in dependency order: Postgres and Redis must report healthy before the API starts,
|
||||||
because the API runs database migrations on boot. A full start takes about **40 seconds**.
|
because the API runs database migrations on boot. A full start takes about **40 seconds**.
|
||||||
|
|
||||||
```zsh title="Terminal"
|
```zsh title="Terminal"
|
||||||
@@ -396,7 +396,7 @@ cio-postgres Up About a minute (healthy)
|
|||||||
cio-redis Up About a minute (healthy)
|
cio-redis Up About a minute (healthy)
|
||||||
```
|
```
|
||||||
|
|
||||||
`cio-minio-init` is absent on purpose — it creates the storage buckets and exits. Confirm it worked:
|
`cio-minio-init` is absent on purpose: it creates the storage buckets and exits. Confirm it worked:
|
||||||
|
|
||||||
```zsh title="Terminal"
|
```zsh title="Terminal"
|
||||||
docker logs cio-minio-init | tail -4
|
docker logs cio-minio-init | tail -4
|
||||||
@@ -425,19 +425,19 @@ for p in 3081 3082 9000 9001; do
|
|||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
All four must report `no response`. If any is reachable, your Compose version ignored `!override` —
|
All four must report `no response`. If any is reachable, your Compose version ignored `!override`.
|
||||||
check that `docker compose version` is v2.24.0 or newer.
|
Check that `docker compose version` is v2.24.0 or newer.
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step>
|
<Step>
|
||||||
### Sign up
|
### Sign up
|
||||||
|
|
||||||
Open `https://app.<your-domain>` and create the first account — it becomes the organization
|
Open `https://app.<your-domain>` and create the first account: it becomes the organization
|
||||||
**admin**. Every signup after that joins as a student unless you invite them with a specific role.
|
**admin**. Every signup after that joins as a student unless you invite them with a specific role.
|
||||||
|
|
||||||
Then verify uploads end to end: create a course, upload an image, and upload a short video. Watch the
|
Then verify uploads end to end: create a course, upload an image, and upload a short video. Watch the
|
||||||
worker process it with `dc logs -f jobs`. In your browser's DevTools the media URLs must point at
|
worker process it with `dc logs -f jobs`. In your browser's DevTools the media URLs must point at
|
||||||
`https://s3.<your-domain>/...` — if you see `localhost:9000`, `OBJECT_STORAGE_PUBLIC_ENDPOINT` is
|
`https://s3.<your-domain>/...`. If you see `localhost:9000`, `OBJECT_STORAGE_PUBLIC_ENDPOINT` is
|
||||||
wrong.
|
wrong.
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
@@ -453,7 +453,7 @@ dc down # stop, keeping all data
|
|||||||
```
|
```
|
||||||
|
|
||||||
:::danger[Never use `dc down -v`]
|
:::danger[Never use `dc down -v`]
|
||||||
The `-v` flag deletes the `postgres-data`, `redis-data` and `minio-data` volumes — your database and
|
The `-v` flag deletes the `postgres-data`, `redis-data` and `minio-data` volumes: your database and
|
||||||
every upload. Plain `dc down` is always safe.
|
every upload. Plain `dc down` is always safe.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -498,7 +498,7 @@ own. **For production, use your own domain.**
|
|||||||
```
|
```
|
||||||
|
|
||||||
3. Change the two site addresses in `~/classroomio/Caddyfile`.
|
3. Change the two site addresses in `~/classroomio/Caddyfile`.
|
||||||
4. Apply, then clear cached media URLs — presigned URLs are cached in Redis for 50 minutes, so
|
4. Apply, then clear cached media URLs: presigned URLs are cached in Redis for 50 minutes, so
|
||||||
without this the old hostname keeps being served:
|
without this the old hostname keeps being served:
|
||||||
|
|
||||||
```zsh title="Terminal"
|
```zsh title="Terminal"
|
||||||
@@ -506,10 +506,14 @@ own. **For production, use your own domain.**
|
|||||||
docker exec cio-redis sh -c "redis-cli --scan --pattern 'presign:download:*' | xargs -r redis-cli del"
|
docker exec cio-redis sh -c "redis-cli --scan --pattern 'presign:download:*' | xargs -r redis-cli del"
|
||||||
```
|
```
|
||||||
|
|
||||||
Existing uploads keep working — media URLs are generated per request from the new endpoint.
|
Existing uploads keep working. Media URLs are generated per request from the new endpoint.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
These are the droplet-specific issues. For general stack problems (sign-in errors right after
|
||||||
|
startup, stuck uploads, OOM kills, disk space), see the shared
|
||||||
|
[Troubleshooting](/self-hosted/troubleshooting) page.
|
||||||
|
|
||||||
| Symptom | Cause |
|
| Symptom | Cause |
|
||||||
| ------- | ----- |
|
| ------- | ----- |
|
||||||
| `permission denied` on `/var/run/docker.sock` | Your user isn't in the `docker` group, or you haven't logged out and back in since adding it |
|
| `permission denied` on `/var/run/docker.sock` | Your user isn't in the `docker` group, or you haven't logged out and back in since adding it |
|
||||||
@@ -549,8 +553,8 @@ This deployment reads the same variables as every other self-hosted install:
|
|||||||
|
|
||||||
## Upgrades
|
## Upgrades
|
||||||
|
|
||||||
Check which versions exist, then pin one — never guess a tag, as an unpublished one fails the pull
|
Check which versions exist, then pin one: never guess a tag, since an unpublished one fails the pull
|
||||||
with `manifest unknown`:
|
with `manifest unknown`.
|
||||||
|
|
||||||
```zsh title="Terminal"
|
```zsh title="Terminal"
|
||||||
curl -s "https://hub.docker.com/v2/repositories/classroomio/dashboard/tags?page_size=20&ordering=last_updated" \
|
curl -s "https://hub.docker.com/v2/repositories/classroomio/dashboard/tags?page_size=20&ordering=last_updated" \
|
||||||
@@ -561,5 +565,5 @@ dc pull && dc up -d
|
|||||||
```
|
```
|
||||||
|
|
||||||
Migrations run automatically on API startup. Roll back by setting `CIO_VERSION` to the previous
|
Migrations run automatically on API startup. Roll back by setting `CIO_VERSION` to the previous
|
||||||
release and repeating — which is why you pin an exact version rather than `latest`. See
|
release and repeating, which is why you pin an exact version rather than `latest`. See
|
||||||
[Configuration → Versions & upgrades](/self-hosted/configuration/versions).
|
[Configuration → Versions & upgrades](/self-hosted/configuration/versions).
|
||||||
|
|||||||
@@ -1,37 +1,129 @@
|
|||||||
---
|
---
|
||||||
title: Docker
|
title: Docker
|
||||||
description: Self-host ClassroomIO with Docker — pull pre-built images (recommended) or build from source.
|
description: Self-host ClassroomIO with Docker — one script installs, starts, upgrades, and backs up the whole stack.
|
||||||
---
|
---
|
||||||
|
|
||||||
Docker is one way to self-host ClassroomIO. There are two paths:
|
Run ClassroomIO anywhere Docker runs. One script manages the entire lifecycle: you copy it,
|
||||||
|
run it, and pick a number. New to self-hosting? Start with
|
||||||
- **Run from pre-built images (recommended)** — pull the published `classroomio/{api,dashboard,jobs}` images. You only need one compose file and a `.env`; no repository clone and no heavy local build.
|
[Self-hosting 101](/self-hosted/101) to see what you're deploying.
|
||||||
- **Build from source** — clone the repo and build the images yourself (needs ~8 GB RAM for the dashboard build).
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Run from Pre-built Images (recommended)
|
## Prerequisites
|
||||||
|
|
||||||
### Prerequisites
|
- Docker installed and running (Compose v2 or the legacy `docker-compose` both work).
|
||||||
|
- A server that meets the [requirements](/self-hosted#server-requirements) (8 GB RAM recommended).
|
||||||
|
|
||||||
- Docker installed and running.
|
## Quick start
|
||||||
|
|
||||||
### Steps
|
<Steps>
|
||||||
|
<Step>
|
||||||
1. Download the pull-based compose file and an env template:
|
### Get the script
|
||||||
|
|
||||||
```zsh title="Terminal"
|
```zsh title="Terminal"
|
||||||
curl -O https://raw.githubusercontent.com/classroomio/classroomio/main/docker-compose.images.yaml
|
mkdir classroomio && cd classroomio
|
||||||
curl -o .env https://raw.githubusercontent.com/classroomio/classroomio/main/.env.example
|
curl -fsSLO https://classroomio.com/classroomio.sh
|
||||||
|
chmod +x classroomio.sh
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Run it and pick Install
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
./classroomio.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Edit `.env`. At minimum set `CIO_VERSION` (pin a release), `DASHBOARD_ORIGIN`, the auth secrets, and SMTP:
|
Choose **1) Install**. The script downloads the compose file, creates `.env` from the template,
|
||||||
|
generates the auth secrets and MinIO credentials for you, then pulls the images and starts the
|
||||||
|
stack. Re-running Install is safe: it never overwrites an existing `.env` or compose file.
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Edit the required values
|
||||||
|
|
||||||
|
Everything else is auto-generated or defaulted. For a real deployment, set only:
|
||||||
|
|
||||||
|
```zsh title=".env"
|
||||||
|
CIO_VERSION=1.0.0 # pin a release for production
|
||||||
|
DASHBOARD_ORIGIN=https://app.yourdomain.com # your public https:// URL
|
||||||
|
POSTGRES_PASSWORD=<strong password> # required in production
|
||||||
|
SMTP_HOST= # email — signup, resets, invites
|
||||||
|
SMTP_PORT=
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_SENDER=
|
||||||
|
```
|
||||||
|
|
||||||
|
Then apply: `./classroomio.sh restart`
|
||||||
|
|
||||||
|
:::warning[Changing `POSTGRES_PASSWORD` after Install does nothing on its own]
|
||||||
|
Postgres only applies `POSTGRES_PASSWORD` the first time it initializes its data volume. If
|
||||||
|
you're editing `.env` after Install has already run, see [rotating it after
|
||||||
|
install](/self-hosted/configuration/secrets#rotating-postgres_password-after-install) —
|
||||||
|
just restarting/recreating containers will break the DB connection instead of fixing it.
|
||||||
|
:::
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
The dashboard is at http://localhost:3082, the API at http://localhost:3081, and the MinIO console
|
||||||
|
at http://localhost:9001 (credentials are in your `.env`).
|
||||||
|
|
||||||
|
:::warning[Pin `CIO_VERSION` in production]
|
||||||
|
The default tags track moving builds: `latest` changes on every merge to `main`. Pin an exact
|
||||||
|
release (e.g. `CIO_VERSION=1.4.2`) so an upstream merge can never change your deployment
|
||||||
|
unannounced. The script warns you about this too. See
|
||||||
|
[Versions & upgrades](/self-hosted/configuration/versions).
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::note[Serving on a real domain?]
|
||||||
|
Use an `https://` origin: session cookies are `Secure`, so
|
||||||
|
[login silently fails over plain HTTP](/self-hosted#https-is-required-in-production).
|
||||||
|
:::
|
||||||
|
|
||||||
|
## The menu
|
||||||
|
|
||||||
|
| Option | What it does |
|
||||||
|
|--------|--------------|
|
||||||
|
| 1 Install | First-time setup: fetch files, create `.env`, generate secrets, pull and start. |
|
||||||
|
| 2 Start | Start the stack (also fills in any missing secrets first). |
|
||||||
|
| 3 Stop | Stop containers. Data volumes are kept. |
|
||||||
|
| 4 Restart | Restart containers, e.g. after editing `.env`. |
|
||||||
|
| 5 Upgrade | **Backs up first**, then pulls newer images and restarts. |
|
||||||
|
| 6 View logs | Follow logs for all services or one (`./classroomio.sh logs api`). |
|
||||||
|
| 7 Backup | Dump Postgres and archive the MinIO volume into `./backups/`. See [Backups](/self-hosted/backups). |
|
||||||
|
|
||||||
|
Every option is also a subcommand (`./classroomio.sh upgrade`), so you can script or cron it.
|
||||||
|
|
||||||
|
<Expandable title="Build from source instead of pulling images">
|
||||||
|
|
||||||
|
To run local code changes, clone the repo and use the same script with `--build`
|
||||||
|
(needs `git` and ~8 GB RAM for the dashboard build):
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
git clone https://github.com/classroomio/classroomio.git
|
||||||
|
cd classroomio
|
||||||
|
cp .env.example .env
|
||||||
|
./classroomio.sh start --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Without `--build` the script pulls the published `classroomio/*` images at your `CIO_VERSION`,
|
||||||
|
which is the faster default. `--no-minio` excludes the bundled MinIO if you use external
|
||||||
|
[S3/R2 storage](/self-hosted/configuration/storage).
|
||||||
|
|
||||||
|
</Expandable>
|
||||||
|
|
||||||
|
## Configuration reference
|
||||||
|
|
||||||
|
Docker self-hosting uses a single root `.env` file: every compose command reads it via
|
||||||
|
`--env-file .env`, and the compose file itself tracks `main` (it's just the stack topology;
|
||||||
|
`CIO_VERSION` pins the app images).
|
||||||
|
|
||||||
|
<Expandable title="Common .env values">
|
||||||
|
|
||||||
```zsh title=".env"
|
```zsh title=".env"
|
||||||
CIO_VERSION=1.0.0 # pin a release (not "latest") so upgrades are deliberate
|
CIO_VERSION=1.0.0 # pin a release (not "latest") so upgrades are deliberate
|
||||||
DASHBOARD_ORIGIN=https://app.yourdomain.com
|
DASHBOARD_ORIGIN=https://app.yourdomain.com
|
||||||
BETTER_AUTH_SECRET=<openssl rand -hex 32>
|
BETTER_AUTH_SECRET= # auto-generated by the script if left blank
|
||||||
PRIVATE_SERVER_KEY=<openssl rand -hex 32> # must be identical in the api and dashboard
|
PRIVATE_SERVER_KEY= # auto-generated; must be identical in api and dashboard
|
||||||
POSTGRES_PASSWORD=<strong password>
|
POSTGRES_PASSWORD=<strong password>
|
||||||
# Email (required for signup/reset/invites)
|
# Email (required for signup/reset/invites)
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
@@ -41,78 +133,13 @@ SMTP_PASSWORD=
|
|||||||
SMTP_SENDER=
|
SMTP_SENDER=
|
||||||
```
|
```
|
||||||
|
|
||||||
:::danger[Set strong secrets — the stack won't start without them]
|
See [`.env.example`](https://github.com/classroomio/classroomio/blob/main/.env.example) for the
|
||||||
This path does **not** run `run-docker-full-stack.sh`, so secrets are not auto-generated. You must set
|
full annotated template.
|
||||||
`BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` yourself, or the compose file fails fast. See
|
|
||||||
[Configuration → Environment & secrets](/self-hosted/configuration/secrets).
|
|
||||||
:::
|
|
||||||
|
|
||||||
3. Pull and start the stack (MinIO is included via the `minio` profile):
|
</Expandable>
|
||||||
|
|
||||||
```zsh title="Terminal"
|
Every variable the stack reads is documented in the shared
|
||||||
docker compose -f docker-compose.images.yaml --env-file .env --profile minio pull
|
[Configuration](/self-hosted/configuration) section:
|
||||||
docker compose -f docker-compose.images.yaml --env-file .env --profile minio up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
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 (log in with `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` from your `.env`)
|
|
||||||
|
|
||||||
## Build from Source
|
|
||||||
|
|
||||||
Use this if you want to build the images locally (e.g. to run code changes).
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Docker Desktop installed and running
|
|
||||||
- `git` installed
|
|
||||||
- ~8 GB RAM available for the dashboard build
|
|
||||||
|
|
||||||
### Steps
|
|
||||||
|
|
||||||
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 `DASHBOARD_ORIGIN` for your domain. For local development the defaults work as-is — the startup script generates `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` when they are missing, and randomizes the MinIO credentials.
|
|
||||||
|
|
||||||
3. Build and start the full stack:
|
|
||||||
|
|
||||||
```zsh title="Terminal"
|
|
||||||
./run-docker-full-stack.sh --build
|
|
||||||
```
|
|
||||||
|
|
||||||
The script builds and starts Postgres, Redis, MinIO (object storage), the API, the dashboard, and the background **jobs** worker.
|
|
||||||
|
|
||||||
:::note[Default is pull, not build]
|
|
||||||
Run without `--build` and the script **pulls** the published `classroomio/*` images at the
|
|
||||||
`CIO_VERSION` in your `.env` instead of building locally — that's the faster default path.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Every variable the stack reads is documented once in the shared
|
|
||||||
[Configuration](/self-hosted/configuration) section — set the ones you need in your root `.env`:
|
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
<CardGroup cols={2}>
|
||||||
<Card title="Environment & secrets" href="/self-hosted/configuration/secrets" icon="key-round">
|
<Card title="Environment & secrets" href="/self-hosted/configuration/secrets" icon="key-round">
|
||||||
@@ -130,51 +157,15 @@ Every variable the stack reads is documented once in the shared
|
|||||||
<Card title="Integrations" href="/self-hosted/configuration/integrations" icon="plug">
|
<Card title="Integrations" href="/self-hosted/configuration/integrations" icon="plug">
|
||||||
Google OAuth, Unsplash, OpenAI, branding.
|
Google OAuth, Unsplash, OpenAI, branding.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="SSO & licensing" href="/self-hosted/configuration/sso" icon="id-card">
|
<Card title="Enterprise & licensing" href="/self-hosted/enterprise" icon="id-card">
|
||||||
`LICENSE_KEY` for Enterprise features.
|
`LICENSE_KEY` for SSO, token auth, and no-tracking.
|
||||||
</Card>
|
</Card>
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
ClassroomIO publishes versioned images and `CIO_VERSION` controls which tag you run. To upgrade the
|
Run `./classroomio.sh upgrade`: it backs up Postgres and MinIO first, then pulls the images at
|
||||||
pre-built images path, bump `CIO_VERSION` and re-pull:
|
your (possibly newly pinned) `CIO_VERSION` and restarts. Rollback = set `CIO_VERSION` back and
|
||||||
|
upgrade again. Details: [Versions & upgrades](/self-hosted/configuration/versions).
|
||||||
|
|
||||||
```zsh title="Terminal"
|
Something not working? See [Troubleshooting](/self-hosted/troubleshooting).
|
||||||
docker compose -f docker-compose.images.yaml --env-file .env --profile minio pull
|
|
||||||
docker compose -f docker-compose.images.yaml --env-file .env --profile minio up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
For the build-from-source path, `git pull` and re-run `./run-docker-full-stack.sh --build`. See
|
|
||||||
[Configuration → Versions & upgrades](/self-hosted/configuration/versions) for pinning, rollback, and
|
|
||||||
the pre-upgrade backup steps.
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
```zsh title="Terminal"
|
|
||||||
# Start the full stack (pulls pre-built images by default)
|
|
||||||
./run-docker-full-stack.sh
|
|
||||||
|
|
||||||
# Build from source instead of pulling
|
|
||||||
./run-docker-full-stack.sh --build
|
|
||||||
|
|
||||||
# Start without MinIO (requires an external S3-compatible store)
|
|
||||||
./run-docker-full-stack.sh --no-minio
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
:::warning[Volume prune deletes data]
|
|
||||||
Avoid `docker volume prune` after `docker compose ... down` — it can remove your database and storage volumes.
|
|
||||||
:::
|
|
||||||
|
|
||||||
Then restart: `./run-docker-full-stack.sh`
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ title: Dokploy
|
|||||||
description: Deploy ClassroomIO on Dokploy using the pull-based Docker Compose stack.
|
description: Deploy ClassroomIO on Dokploy using the pull-based Docker Compose stack.
|
||||||
---
|
---
|
||||||
|
|
||||||
[Dokploy](https://dokploy.com) is an open-source, self-hosted PaaS — a Heroku/Vercel alternative you run
|
[Dokploy](https://dokploy.com) is an open-source, self-hosted PaaS (a Heroku/Vercel alternative you run
|
||||||
on your own VPS. It deploys ClassroomIO from the published `classroomio/*` images using the **same
|
on your own VPS). It deploys ClassroomIO from the published `classroomio/*` images using the **same
|
||||||
pull-based Docker Compose file** as the [Docker](/self-hosted/docker) and [Coolify](/self-hosted/coolify)
|
pull-based Docker Compose file** as the [Docker](/self-hosted/docker) and [Coolify](/self-hosted/coolify)
|
||||||
guides — Dokploy **pulls** the images and never builds anything.
|
guides. Dokploy **pulls** the images and never builds anything.
|
||||||
|
|
||||||
## Watch the walkthrough
|
## Watch the walkthrough
|
||||||
|
|
||||||
@@ -17,18 +17,18 @@ steps below mirror it.
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- A running **Dokploy instance** with a **server connected** to it. Dokploy runs on any VPS —
|
- A running **Dokploy instance** with a **server connected** to it. Dokploy runs on any VPS:
|
||||||
DigitalOcean, Hetzner, AWS (Lightsail/EC2), or another provider. See Dokploy's
|
DigitalOcean, Hetzner, AWS (Lightsail/EC2), or another provider. See Dokploy's
|
||||||
[installation guide](https://docs.dokploy.com/docs/core/installation) to set this up.
|
[installation guide](https://docs.dokploy.com/docs/core/installation) to set this up.
|
||||||
- A server sized for ClassroomIO. Dokploy's own baseline stack needs ~2 GB RAM, and ClassroomIO's
|
- A server sized for ClassroomIO. Dokploy's own baseline stack needs ~2 GB RAM, and ClassroomIO's
|
||||||
seven services need **8 GB for reliable operation** — provision for both. See
|
seven services need **8 GB for reliable operation**, so provision for both. See
|
||||||
[Server requirements](/self-hosted#server-requirements).
|
[Server requirements](/self-hosted#server-requirements).
|
||||||
- A **domain** with a DNS **A record pointing at your server's public IP**. Dokploy's built-in Traefik
|
- A **domain** with a DNS **A record pointing at your server's public IP**. Dokploy's built-in Traefik
|
||||||
proxy terminates HTTPS and issues the certificate.
|
proxy terminates HTTPS and issues the certificate.
|
||||||
- An **SMTP provider** for signup verification, password reset, and invite emails — see
|
- An **SMTP provider** for signup verification, password reset, and invite emails. See
|
||||||
[Configuration → Email (SMTP)](/self-hosted/configuration/email).
|
[Configuration → Email (SMTP)](/self-hosted/configuration/email).
|
||||||
- *Optional:* an **OpenAI API key** (transcription + AI course generation) and Google OAuth / Unsplash
|
- *Optional:* an **OpenAI API key** (transcription + AI course generation) and Google OAuth / Unsplash
|
||||||
keys — see [Configuration → Integrations](/self-hosted/configuration/integrations).
|
keys. See [Configuration → Integrations](/self-hosted/configuration/integrations).
|
||||||
|
|
||||||
:::note[Two secrets to generate first]
|
:::note[Two secrets to generate first]
|
||||||
Dokploy does **not** run our secret-generation script, so generate `BETTER_AUTH_SECRET` and
|
Dokploy does **not** run our secret-generation script, so generate `BETTER_AUTH_SECRET` and
|
||||||
@@ -64,7 +64,7 @@ In the Dokploy dashboard, open **Projects** and click **Create Project**. Give i
|
|||||||
### Add a Compose service
|
### Add a Compose service
|
||||||
|
|
||||||
Inside the project, click **Create Service** and choose **Compose** from the service types. When
|
Inside the project, click **Create Service** and choose **Compose** from the service types. When
|
||||||
prompted, **select the server** you connected to Dokploy — this is the VPS ClassroomIO runs on.
|
prompted, **select the server** you connected to Dokploy. This is the VPS ClassroomIO runs on.
|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
@@ -72,10 +72,10 @@ prompted, **select the server** you connected to Dokploy — this is the VPS Cla
|
|||||||
<Step>
|
<Step>
|
||||||
### Choose the Raw (manual) provider and paste the compose file
|
### Choose the Raw (manual) provider and paste the compose file
|
||||||
|
|
||||||
Dokploy asks where the compose file comes from — GitHub, GitLab, Bitbucket, or **Raw**. Choose **Raw**
|
Dokploy asks where the compose file comes from: GitHub, GitLab, Bitbucket, or **Raw**. Choose **Raw**
|
||||||
(the manual option) and paste the contents of
|
(the manual option) and paste the contents of
|
||||||
[`docker-compose.images.yaml`](https://github.com/classroomio/classroomio/blob/main/docker-compose.images.yaml)
|
[`docker-compose.images.yaml`](https://github.com/classroomio/classroomio/blob/main/docker-compose.images.yaml),
|
||||||
— the same pull-based stack used for the [Docker](/self-hosted/docker) and [Coolify](/self-hosted/coolify)
|
the same pull-based stack used for the [Docker](/self-hosted/docker) and [Coolify](/self-hosted/coolify)
|
||||||
deployments.
|
deployments.
|
||||||
|
|
||||||

|

|
||||||
@@ -101,7 +101,7 @@ SMTP_PASSWORD=<smtp password>
|
|||||||
SMTP_SENDER=ClassroomIO <[email protected]>
|
SMTP_SENDER=ClassroomIO <[email protected]>
|
||||||
```
|
```
|
||||||
|
|
||||||
The full variable reference — object storage, CSP, integrations, licensing, versions — lives in the
|
The full variable reference (object storage, CSP, integrations, licensing, versions) lives in the
|
||||||
shared [Configuration](/self-hosted/configuration) section. Copy what you need before deploying.
|
shared [Configuration](/self-hosted/configuration) section. Copy what you need before deploying.
|
||||||
|
|
||||||

|

|
||||||
@@ -111,7 +111,7 @@ shared [Configuration](/self-hosted/configuration) section. Copy what you need b
|
|||||||
### Point your domain at the server
|
### Point your domain at the server
|
||||||
|
|
||||||
Before the domain will work, add a DNS **A record** for it (e.g. `dash.yourdomain.com`) pointing at
|
Before the domain will work, add a DNS **A record** for it (e.g. `dash.yourdomain.com`) pointing at
|
||||||
your server's public IP. Do this for the dashboard domain — and any other domain you plan to use (such
|
your server's public IP. Do this for the dashboard domain, and any other domain you plan to use (such
|
||||||
as storage). DNS can take a few minutes to propagate.
|
as storage). DNS can take a few minutes to propagate.
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
@@ -121,7 +121,7 @@ as storage). DNS can take a few minutes to propagate.
|
|||||||
|
|
||||||
Click **Deploy**. Dokploy pulls `classroomio/{api,dashboard,jobs}` plus Postgres, Redis, and MinIO,
|
Click **Deploy**. Dokploy pulls `classroomio/{api,dashboard,jobs}` plus Postgres, Redis, and MinIO,
|
||||||
then starts the stack. The API runs database setup on first boot. Watch the **logs** until every
|
then starts the stack. The API runs database setup on first boot. Watch the **logs** until every
|
||||||
service is up — this is your first deploy.
|
service is up. This is your first deploy.
|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
@@ -131,12 +131,12 @@ service is up — this is your first deploy.
|
|||||||
|
|
||||||
Open the service's **Domains** section and click **Add Domain**. In the modal that appears:
|
Open the service's **Domains** section and click **Add Domain**. In the modal that appears:
|
||||||
|
|
||||||
- **Service:** select **dashboard** — the service this domain routes to.
|
- **Service:** select **dashboard**, the service this domain routes to.
|
||||||
- **Host:** your dashboard domain, e.g. `dash.yourdomain.com`.
|
- **Host:** your dashboard domain, e.g. `dash.yourdomain.com`.
|
||||||
- **Container Port:** `3082` — the port the dashboard listens on.
|
- **Container Port:** `3082`, the port the dashboard listens on.
|
||||||
- **HTTPS:** turn it on, then choose **Let's Encrypt** as the certificate provider.
|
- **HTTPS:** turn it on, then choose **Let's Encrypt** as the certificate provider.
|
||||||
|
|
||||||
Create the domain. Dokploy provisions the certificate through Traefik — it usually only takes a moment.
|
Create the domain. Dokploy provisions the certificate through Traefik, which usually only takes a moment.
|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
@@ -174,7 +174,7 @@ shared [Configuration](/self-hosted/configuration) section:
|
|||||||
<Card title="Integrations" href="/self-hosted/configuration/integrations" icon="plug">
|
<Card title="Integrations" href="/self-hosted/configuration/integrations" icon="plug">
|
||||||
Google OAuth, Unsplash, OpenAI.
|
Google OAuth, Unsplash, OpenAI.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="SSO & licensing" href="/self-hosted/configuration/sso" icon="id-card">
|
<Card title="Enterprise & licensing" href="/self-hosted/enterprise" icon="id-card">
|
||||||
`LICENSE_KEY` for Enterprise features.
|
`LICENSE_KEY` for Enterprise features.
|
||||||
</Card>
|
</Card>
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Enterprise & licensing
|
||||||
|
description: What a ClassroomIO enterprise license unlocks on self-hosted — SSO, token auth, no-tracking — and how LICENSE_KEY validation works.
|
||||||
|
---
|
||||||
|
|
||||||
|
ClassroomIO is [open core](/self-hosted/101#how-licensing-works): the complete LMS is free to
|
||||||
|
self-host, and a small set of enterprise features is unlocked by a paid license key. No key means
|
||||||
|
everything else still works, so leave `LICENSE_KEY` unset until you need one of these.
|
||||||
|
|
||||||
|
## What a license unlocks
|
||||||
|
|
||||||
|
| Feature | What it does |
|
||||||
|
|---------|--------------|
|
||||||
|
| **SSO** | Single Sign-On with OIDC (Okta, Azure AD, and more). Setup: [Enterprise SSO](/enterprise-sso). |
|
||||||
|
| **Token auth** | Issue signed tokens to log users in from your own app. Setup: [Token Auth](/token-auth). |
|
||||||
|
| **No-tracking** | Disables product analytics in the dashboard (PostHog, Umami) and third-party embeds. Without this feature, self-hosted mode already disables error reporting and the feedback widget, but not product analytics. |
|
||||||
|
|
||||||
|
Without a valid license, the SSO and token-auth APIs return `403 FEATURE_REQUIRES_LICENSE` and
|
||||||
|
their settings are hidden in the dashboard UI.
|
||||||
|
|
||||||
|
## Setting `LICENSE_KEY`
|
||||||
|
|
||||||
|
`LICENSE_KEY` is read by the **api** service. Set it wherever that service takes its environment:
|
||||||
|
|
||||||
|
| Deployment | Where to set it |
|
||||||
|
|------------|-----------------|
|
||||||
|
| Docker | repository root `.env` |
|
||||||
|
| Railway | the `cio-api` service environment |
|
||||||
|
| Coolify / Dokploy | the resource's environment variables |
|
||||||
|
| Local development | `apps/api/.env` |
|
||||||
|
|
||||||
|
```zsh title=".env"
|
||||||
|
LICENSE_KEY=<your enterprise license key>
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart the stack with `./classroomio.sh restart` after setting it (this recreates the
|
||||||
|
`api` container so it picks up the new value — `docker compose restart api` on its own
|
||||||
|
does **not**, since it doesn't recreate the container).
|
||||||
|
|
||||||
|
## How validation works
|
||||||
|
|
||||||
|
The api service validates the key against ClassroomIO's license server
|
||||||
|
(`enterprise-api.classroomio.dev`) and caches the result in memory for **one hour**.
|
||||||
|
|
||||||
|
What that means operationally:
|
||||||
|
|
||||||
|
- **The api container needs outbound HTTPS** to that host. In an air-gapped or egress-restricted
|
||||||
|
network, licensed features can't activate. Contact ClassroomIO about your environment before
|
||||||
|
buying.
|
||||||
|
- If a check fails (network blip, license server unreachable), licensed features switch off and
|
||||||
|
the failure is also cached, so it can take **up to an hour** (or an api restart) after
|
||||||
|
connectivity returns before they come back.
|
||||||
|
- Everything outside the licensed feature list is unaffected by license-server reachability.
|
||||||
|
|
||||||
|
## Getting a license
|
||||||
|
|
||||||
|
[Book a call](https://cal.com/classroomio/enterprise) to get an enterprise license key.
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: Self Hosting
|
title: Self Hosting
|
||||||
description: Run ClassroomIO on your own infrastructure — with Docker, DigitalOcean, Railway, Coolify, or Dokploy.
|
description: Run ClassroomIO on your own infrastructure — with Docker, DigitalOcean, Railway, Coolify, or Dokploy.
|
||||||
|
sidebar:
|
||||||
|
label: Overview
|
||||||
---
|
---
|
||||||
|
|
||||||
ClassroomIO is open source, and you can run the whole platform on your own infrastructure. Every
|
ClassroomIO is open source, and you can run the whole platform on your own infrastructure. Every
|
||||||
option below runs the same stack — the API, the dashboard, and the background jobs worker, backed by
|
option below runs the same stack: the API, the dashboard, and the background jobs worker, backed by
|
||||||
Postgres and Redis.
|
Postgres and Redis.
|
||||||
|
|
||||||
Pick the path that matches where you want it to run.
|
Pick the path that matches where you want it to run.
|
||||||
@@ -14,7 +16,7 @@ Pick the path that matches where you want it to run.
|
|||||||
Run anywhere Docker runs. Pull the pre-built images, or build from source.
|
Run anywhere Docker runs. Pull the pre-built images, or build from source.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="DigitalOcean" href="/self-hosted/digitalocean" icon="droplet">
|
<Card title="DigitalOcean" href="/self-hosted/digitalocean" icon="droplet">
|
||||||
A plain VPS, end to end — Docker Compose plus Caddy for automatic HTTPS.
|
A plain VPS, end to end: Docker Compose plus Caddy for automatic HTTPS.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Railway" href="/self-hosted/railway" icon="train-front">
|
<Card title="Railway" href="/self-hosted/railway" icon="train-front">
|
||||||
Deploy to Railway from a template. The quickest managed option.
|
Deploy to Railway from a template. The quickest managed option.
|
||||||
@@ -49,16 +51,18 @@ pick, have mail credentials ready before you start.
|
|||||||
- Object storage (**MinIO** or S3-compatible) if you want file and video uploads.
|
- Object storage (**MinIO** or S3-compatible) if you want file and video uploads.
|
||||||
- *Optional:* an **OpenAI API key** for transcription and AI course generation.
|
- *Optional:* an **OpenAI API key** for transcription and AI course generation.
|
||||||
|
|
||||||
For the full environment variable reference — secrets, email, storage, CSP, integrations, licensing,
|
The [Configuration](/self-hosted/configuration) section has the full environment variable reference
|
||||||
and versions — see the [Configuration](/self-hosted/configuration) section, which documents every
|
(secrets, email, storage, CSP, integrations, licensing, versions) and documents every variable the
|
||||||
variable the stack reads.
|
stack reads.
|
||||||
|
|
||||||
## Server requirements
|
## Server requirements
|
||||||
|
|
||||||
ClassroomIO is not a single container — it runs **seven services**: Postgres, Redis, MinIO (object
|
ClassroomIO is not a single container. It runs **seven services**: Postgres, Redis, MinIO (object
|
||||||
storage), the API, the dashboard, and a background **jobs worker** that handles video processing,
|
storage), the API, the dashboard, a background **jobs worker** that handles video processing,
|
||||||
transcription, and AI course generation. The jobs worker spikes RAM whenever it runs, so a server's
|
transcription, and AI course generation, and **`minio-init`**, a one-shot container that creates
|
||||||
idle memory use understates what you actually need.
|
the MinIO buckets on first start and then exits, so it won't show up in `docker ps` afterward. The
|
||||||
|
jobs worker spikes RAM whenever it runs, so a server's idle memory use understates what you
|
||||||
|
actually need.
|
||||||
|
|
||||||
| Resource | Minimum | Recommended |
|
| Resource | Minimum | Recommended |
|
||||||
| -------- | ---------------- | ------------- |
|
| -------- | ---------------- | ------------- |
|
||||||
@@ -67,31 +71,30 @@ idle memory use understates what you actually need.
|
|||||||
| Disk | 20 GB + uploads | 40 GB+ SSD |
|
| Disk | 20 GB + uploads | 40 GB+ SSD |
|
||||||
|
|
||||||
:::warning[Don't under-provision RAM]
|
:::warning[Don't under-provision RAM]
|
||||||
A **1 GB server cannot run ClassroomIO at all** — it exhausts memory on the base stack before the
|
A **1 GB** server can't run ClassroomIO at all: it exhausts memory on the base stack before the app
|
||||||
app is even up. **2 GB is only enough for a PaaS control plane** (Coolify, Dokploy) with little left
|
is even up. **2 GB** is only enough for a PaaS control plane (Coolify, Dokploy), with little left
|
||||||
over for the app. Target **8 GB for reliable operation**, especially once the jobs worker starts
|
over for the app itself. Aim for **8 GB** for reliable operation, especially once the jobs worker
|
||||||
processing video or AI generation.
|
starts processing video or AI generation.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::note[Running a PaaS on the same box?]
|
:::note[Running a PaaS on the same box?]
|
||||||
Coolify and Dokploy need roughly **2 GB for themselves** on top of ClassroomIO's requirement. If
|
Coolify and Dokploy need roughly **2 GB** for themselves on top of ClassroomIO's requirement. If
|
||||||
they share the server, size it for both — e.g. 8 GB for ClassroomIO plus the PaaS overhead.
|
they share the server, size it for both, e.g. 8 GB for ClassroomIO plus the PaaS overhead.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Building the images **from source** is a separate, heavier job that needs ~8 GB RAM at build time —
|
Building the images **from source** is a separate, heavier job that needs ~8 GB RAM at build time.
|
||||||
see the [Docker guide](/self-hosted/docker#build-from-source). Pulling the pre-built images (the
|
See the [Docker guide](/self-hosted/docker). Pulling the pre-built images (the default) avoids that.
|
||||||
default) avoids that.
|
|
||||||
|
|
||||||
## HTTPS is required in production
|
## HTTPS is required in production
|
||||||
|
|
||||||
:::warning[Serve over HTTPS on any real domain]
|
:::warning[Serve over HTTPS on any real domain]
|
||||||
ClassroomIO's authentication session cookies are marked `Secure`, so browsers refuse to store or
|
ClassroomIO's authentication session cookies are marked `Secure`, so browsers refuse to store or
|
||||||
send them over plain HTTP — **login silently fails**. Plain HTTP works only for local evaluation on
|
send them over plain HTTP. **Login silently fails**. Plain HTTP works only for local evaluation on
|
||||||
`localhost`. Any internet-facing deployment **must** be served over HTTPS.
|
`localhost`. Any internet-facing deployment **must** be served over HTTPS.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Terminate TLS at a reverse proxy in front of the stack:
|
Terminate TLS at a reverse proxy in front of the stack:
|
||||||
|
|
||||||
- **Railway** and **Coolify** issue the certificate and terminate HTTPS for you — nothing extra to do.
|
- **Railway** and **Coolify** issue the certificate and terminate HTTPS for you: nothing extra to do.
|
||||||
- **Raw Docker** on your own server: put **nginx**, **Caddy**, or **Traefik** in front, and set
|
- **Raw Docker** on your own server: put **nginx**, **Caddy**, or **Traefik** in front, and set
|
||||||
`DASHBOARD_ORIGIN` / `ORIGIN` to the `https://` URL your users visit.
|
`DASHBOARD_ORIGIN` / `ORIGIN` to the `https://` URL your users visit.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Prefer to follow along? This video walks through the whole deployment end to end
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- A **Railway account** (a paid plan is recommended — the stack runs several always-on services).
|
- A **Railway account** (a paid plan is recommended, since the stack runs several always-on services).
|
||||||
- An **SMTP provider** (or Zoho/ZeptoMail) for signup verification, password reset, and invite emails. See [SMTP Setup](#smtp-setup).
|
- An **SMTP provider** (or Zoho/ZeptoMail) for signup verification, password reset, and invite emails. See [SMTP Setup](#smtp-setup).
|
||||||
- *Optional:* a **custom domain** for the dashboard and storage, an **OpenAI API key** (transcription + AI course generation), and Google OAuth / Unsplash keys.
|
- *Optional:* a **custom domain** for the dashboard and storage, an **OpenAI API key** (transcription + AI course generation), and Google OAuth / Unsplash keys.
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ See [Configuration → Environment & secrets](/self-hosted/configuration/secrets
|
|||||||
|
|
||||||
## Step-by-step walkthrough
|
## Step-by-step walkthrough
|
||||||
|
|
||||||
The one-click template provisions **all of the services** for you — Postgres, Redis, `cio-api`, `cio-dashboard`, `cio-jobs`, and `cio-minio` (object storage). The flow is: **configure a few values → deploy → connect the dashboard domain → sign up**.
|
The one-click template provisions **all of the services** for you: Postgres, Redis, `cio-api`, `cio-dashboard`, `cio-jobs`, and `cio-minio` (object storage). The flow is: **configure a few values → deploy → connect the dashboard domain → sign up**.
|
||||||
|
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
@@ -49,10 +49,10 @@ Click the **Deploy on Railway** button above. Railway opens the template page wh
|
|||||||
<Step>
|
<Step>
|
||||||
### Review the preconfigured services
|
### Review the preconfigured services
|
||||||
|
|
||||||
The template comes with every service preconfigured — most need nothing from you:
|
The template comes with every service preconfigured: most need nothing from you:
|
||||||
|
|
||||||
- **Redis** and **Postgres** — no configuration required.
|
- **Redis** and **Postgres**: no configuration required.
|
||||||
- **cio-dashboard** — no required configuration (optionally set branding such as the public OG image; not required).
|
- **cio-dashboard**: no required configuration (optionally set branding such as the public OG image; not required).
|
||||||
|
|
||||||
The next three steps cover the services that **do** need input: `cio-jobs`, `cio-minio`, and `cio-api`.
|
The next three steps cover the services that **do** need input: `cio-jobs`, `cio-minio`, and `cio-api`.
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ The next three steps cover the services that **do** need input: `cio-jobs`, `cio
|
|||||||
|
|
||||||
Set the storage credentials on `cio-minio`:
|
Set the storage credentials on `cio-minio`:
|
||||||
|
|
||||||
- `MINIO_SERVER_URL` — optional but recommended (fixes MinIO console redirects behind the proxy); the template presets it to the generated `cio-minio` domain, so you normally just confirm it
|
- `MINIO_SERVER_URL`: optional but recommended (fixes MinIO console redirects behind the proxy); the template presets it to the generated `cio-minio` domain, so you normally just confirm it
|
||||||
- `MINIO_ROOT_USER`
|
- `MINIO_ROOT_USER`
|
||||||
- `MINIO_ROOT_PASSWORD`
|
- `MINIO_ROOT_PASSWORD`
|
||||||
|
|
||||||
@@ -97,16 +97,16 @@ On `cio-api`, set the SMTP host and any additional values you want. See the [Env
|
|||||||
<Step>
|
<Step>
|
||||||
### Deploy
|
### Deploy
|
||||||
|
|
||||||
Click **Deploy**. Railway begins building and bringing the services online — Redis, Postgres, and the rest come up first.
|
Click **Deploy**. Railway begins building and bringing the services online: Redis, Postgres, and the rest come up first.
|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step>
|
<Step>
|
||||||
### First boot may crash — this is expected
|
### First boot may crash: this is expected
|
||||||
|
|
||||||
:::warning[Only if you use the Railway-generated subdomain]
|
:::warning[Only if you use the Railway-generated subdomain]
|
||||||
On the first deploy, `cio-api` can crash. This is expected **only when you rely on the Railway-generated subdomain**: the API needs the dashboard's public domain (Better Auth's base URL), which doesn't exist yet. If you're using your **own custom subdomain**, this crash doesn't happen — skip ahead to signing up.
|
On the first deploy, `cio-api` can crash. This is expected **only when you rely on the Railway-generated subdomain**: the API needs the dashboard's public domain (Better Auth's base URL), which doesn't exist yet. If you're using your **own custom subdomain**, this crash doesn't happen. Skip ahead to signing up.
|
||||||
:::
|
:::
|
||||||
|
|
||||||

|

|
||||||
@@ -141,7 +141,7 @@ Open the generated dashboard URL in a **new browser tab** and **sign up** to cre
|
|||||||
|
|
||||||
## Environment variable reference
|
## Environment variable reference
|
||||||
|
|
||||||
You set these values **while configuring each service on the template page** (the configure steps above) — you don't enter them again after deploying. Use this section as the reference for what each service's variables should be.
|
You set these values **while configuring each service on the template page** (the configure steps above); you don't enter them again after deploying. Use this section as the reference for what each service's variables should be.
|
||||||
|
|
||||||
### cio-api
|
### cio-api
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ Optional: add `PUBLIC_APP_TITLE`, `PUBLIC_APP_DESCRIPTION`, `PUBLIC_OG_IMAGE_URL
|
|||||||
|
|
||||||
### cio-jobs (background worker)
|
### cio-jobs (background worker)
|
||||||
|
|
||||||
The jobs worker has **no public domain** — it drains the Redis queue (video processing, transcription, AI course generation, and emails). It shares the same Postgres, Redis, storage, email, and OpenAI settings as the API:
|
The jobs worker has **no public domain**: it drains the Redis queue (video processing, transcription, AI course generation, and emails). It shares the same Postgres, Redis, storage, email, and OpenAI settings as the API:
|
||||||
|
|
||||||
```zsh title="env"
|
```zsh title="env"
|
||||||
DATABASE_URL="${{db.DATABASE_URL}}"
|
DATABASE_URL="${{db.DATABASE_URL}}"
|
||||||
@@ -225,7 +225,7 @@ OPENAI_API_KEY=
|
|||||||
|
|
||||||
### cio-minio (object storage)
|
### cio-minio (object storage)
|
||||||
|
|
||||||
`cio-minio` ships **in the template** — its `/data` volume is already attached and the `videos`,
|
`cio-minio` ships **in the template**: its `/data` volume is already attached and the `videos`,
|
||||||
`documents`, and `media` buckets are already seeded. You only set its credentials during
|
`documents`, and `media` buckets are already seeded. You only set its credentials during
|
||||||
configuration:
|
configuration:
|
||||||
|
|
||||||
@@ -239,11 +239,11 @@ MINIO_SERVER_URL=https://${{cio-minio.RAILWAY_PUBLIC_DOMAIN}}
|
|||||||
|
|
||||||
The api and jobs services reach MinIO over **two** endpoints: the private `OBJECT_STORAGE_ENDPOINT`
|
The api and jobs services reach MinIO over **two** endpoints: the private `OBJECT_STORAGE_ENDPOINT`
|
||||||
(server uploads/reads) and the public `OBJECT_STORAGE_PUBLIC_ENDPOINT` + `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL`
|
(server uploads/reads) and the public `OBJECT_STORAGE_PUBLIC_ENDPOINT` + `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL`
|
||||||
(browser-facing presigned downloads and public images) — already wired in the env blocks above.
|
(browser-facing presigned downloads and public images), already wired in the env blocks above.
|
||||||
`OBJECT_STORAGE_FORCE_PATH_STYLE=true` is required for MinIO.
|
`OBJECT_STORAGE_FORCE_PATH_STYLE=true` is required for MinIO.
|
||||||
|
|
||||||
:::warning[MinIO is your durability boundary]
|
:::warning[MinIO is your durability boundary]
|
||||||
Single-node MinIO has no redundancy — the Railway Volume is the only thing protecting your uploads, so
|
Single-node MinIO has no redundancy: the Railway Volume is the only thing protecting your uploads, so
|
||||||
back it up. See [Configuration → Object storage & uploads](/self-hosted/configuration/storage) for
|
back it up. See [Configuration → Object storage & uploads](/self-hosted/configuration/storage) for
|
||||||
external Cloudflare R2 / AWS S3 alternatives.
|
external Cloudflare R2 / AWS S3 alternatives.
|
||||||
:::
|
:::
|
||||||
@@ -254,7 +254,7 @@ See [Configuration → Email (SMTP)](/self-hosted/configuration/email) for the S
|
|||||||
|
|
||||||
### SSO (Enterprise)
|
### SSO (Enterprise)
|
||||||
|
|
||||||
SSO requires a `LICENSE_KEY` on the `cio-api` service. See [Configuration → SSO & licensing](/self-hosted/configuration/sso).
|
SSO requires a `LICENSE_KEY` on the `cio-api` service. See [Enterprise & licensing](/self-hosted/enterprise).
|
||||||
|
|
||||||
## Upgrades
|
## Upgrades
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
title: Troubleshooting
|
||||||
|
description: Diagnose common self-hosted ClassroomIO failures — sign-in errors, stuck uploads, out-of-memory kills, and disk issues.
|
||||||
|
---
|
||||||
|
|
||||||
|
Start here when something misbehaves. Every entry below is a failure mode we've seen in real
|
||||||
|
deployments, with its cause and fix.
|
||||||
|
|
||||||
|
First, always check what's actually running:
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
./classroomio.sh logs # follow all service logs
|
||||||
|
docker ps # anything restarting or exited?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick reference
|
||||||
|
|
||||||
|
| Symptom | Likely cause | Fix |
|
||||||
|
|---------|-------------|-----|
|
||||||
|
| Sign-in fails right after startup with a database error (`57P03`) | Postgres is still starting | Wait 30–60 s and retry, [details](#sign-in-fails-with-57p03-right-after-startup) |
|
||||||
|
| Login silently fails on your domain (no error, just back to the login page) | Serving over plain HTTP — session cookies are `Secure` | Serve over HTTPS — [details](/self-hosted#https-is-required-in-production) |
|
||||||
|
| Login loops or CSRF errors | `DASHBOARD_ORIGIN` doesn't match the URL users actually visit | Set it to the exact `https://` origin, restart |
|
||||||
|
| Uploads stuck at "processing" forever, no error | The `jobs` worker isn't running, or Redis is unreachable | [details](#uploads-stuck-at-processing) |
|
||||||
|
| Containers randomly killed, stack unstable | Out of memory — a transcode can exhaust a 4 GB server | [details](#containers-are-oom-killed) |
|
||||||
|
| `No space left on device`, MinIO "minimum free drive threshold" | Disk full (images, build cache, uploads) | [details](#no-space-left-on-device) |
|
||||||
|
| API container exits immediately at boot | Database migration failed | [details](#api-container-exits-at-boot) |
|
||||||
|
| No signup / invite / reset emails, logs show `ECONNREFUSED` | SMTP not configured or wrong host/port | Check [Email settings](/self-hosted/configuration/email) |
|
||||||
|
| Videos never get captions; no transcription | `OPENAI_API_KEY` not set | [details](#transcription-never-happens) |
|
||||||
|
| Crash loops with `read ECONNABORTED` during managed-Redis maintenance | A bug fixed upstream | Upgrade — [details](#crash-loops-during-redis-maintenance) |
|
||||||
|
|
||||||
|
## Sign-in fails with 57P03 right after startup
|
||||||
|
|
||||||
|
**Symptom:** immediately after starting the stack, sign-in returns a database error with Postgres
|
||||||
|
code `57P03` ("the database system is in recovery mode" / "is starting up").
|
||||||
|
|
||||||
|
**Cause:** Postgres reports itself ready (`pg_isready` passes) before it has finished crash
|
||||||
|
recovery, so the API can connect but queries fail.
|
||||||
|
|
||||||
|
**Fix:** wait 30–60 seconds and retry. Recovery completes on its own. Current compose files
|
||||||
|
already guard against this: the Postgres healthcheck requires a successful `SELECT 1`, not just
|
||||||
|
`pg_isready`, so dependent services wait until queries actually work. If you see this repeatedly,
|
||||||
|
your compose file predates that healthcheck. Refresh it:
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
curl -fsSLO https://raw.githubusercontent.com/classroomio/classroomio/main/docker-compose.images.yaml
|
||||||
|
./classroomio.sh restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uploads stuck at "processing"
|
||||||
|
|
||||||
|
Video processing, transcription, AI course generation, and most emails don't run in the API. They
|
||||||
|
are queued in Redis and executed by the separate **jobs** worker. If that worker is down (or the API
|
||||||
|
has no `REDIS_URL`), jobs are created but nothing ever picks them up: uploads sit at "processing"
|
||||||
|
with no error anywhere in the UI.
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
docker ps | grep jobs # is cio-jobs up?
|
||||||
|
./classroomio.sh logs jobs # any errors?
|
||||||
|
```
|
||||||
|
|
||||||
|
The bundled compose file always starts the worker; on PaaS platforms make sure the jobs service
|
||||||
|
wasn't scaled to zero or crashed on boot.
|
||||||
|
|
||||||
|
## Containers are OOM-killed
|
||||||
|
|
||||||
|
The jobs worker spikes RAM while processing video, enough to get Postgres or other containers
|
||||||
|
killed on a 4 GB server even though the stack idles comfortably. See
|
||||||
|
[server requirements](/self-hosted#server-requirements).
|
||||||
|
|
||||||
|
- Confirm with `docker inspect <container> | grep -i oom` and `free -h`.
|
||||||
|
- Add swap as a buffer, or resize to 8 GB for reliable operation.
|
||||||
|
|
||||||
|
## "No space left on device"
|
||||||
|
|
||||||
|
Also surfaces as MinIO's "Storage reached its minimum free drive threshold".
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
df -h # check host disk
|
||||||
|
docker system df # check Docker usage
|
||||||
|
docker builder prune -f # free build cache
|
||||||
|
docker image prune -a -f # free unused images
|
||||||
|
docker container prune -f # free stopped containers
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart: `./classroomio.sh start`
|
||||||
|
|
||||||
|
:::danger[Never use `docker volume prune` or `down -v`]
|
||||||
|
`docker volume prune` removes volumes not attached to a running container: run after
|
||||||
|
`docker compose down`, it deletes your **database, Redis, and uploads** (`postgres-data`,
|
||||||
|
`redis-data`, `minio-data`). `docker compose down -v` does the same explicitly. Use only the prune
|
||||||
|
commands above to free space.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## API container exits at boot
|
||||||
|
|
||||||
|
The API runs database migrations before it starts listening. If a migration fails, the container
|
||||||
|
exits rather than serve a half-migrated database. That's deliberate.
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
./classroomio.sh logs api # the migration error is at the end
|
||||||
|
```
|
||||||
|
|
||||||
|
Common causes: wrong `DATABASE_URL`/`POSTGRES_*` credentials after an edit, or Postgres not yet
|
||||||
|
healthy. Fix the cause and `./classroomio.sh start` again. Migrations are safe to re-run.
|
||||||
|
|
||||||
|
## Transcription never happens
|
||||||
|
|
||||||
|
Transcription (video captions) requires `OPENAI_API_KEY` on the **jobs** service. Without it, media
|
||||||
|
jobs still complete (thumbnails and processing work), but the transcription step is skipped with a
|
||||||
|
warning in the jobs logs. Set the key and re-trigger transcription; see
|
||||||
|
[Integrations](/self-hosted/configuration/integrations).
|
||||||
|
|
||||||
|
## Crash loops during Redis maintenance
|
||||||
|
|
||||||
|
Older versions could crash-loop with cascading `read ECONNABORTED` errors when a managed Redis
|
||||||
|
provider (e.g. Redis Cloud) ran scheduled maintenance or failover. This was fixed upstream: the
|
||||||
|
stack now reconnects and resumes automatically. If you hit it, upgrade:
|
||||||
|
|
||||||
|
```zsh title="Terminal"
|
||||||
|
./classroomio.sh upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform-specific issues
|
||||||
|
|
||||||
|
Deploying on a DigitalOcean-style VPS with Caddy? That guide has its own
|
||||||
|
[troubleshooting table](/self-hosted/digitalocean#troubleshooting) covering certificates, firewalls,
|
||||||
|
and proxy issues. For anything else, [open an issue](https://github.com/classroomio/classroomio/issues)
|
||||||
|
and include the output of `./classroomio.sh logs`.
|
||||||
@@ -12,7 +12,7 @@ Token Auth requires an **Enterprise** plan.
|
|||||||
|
|
||||||
**Cloud:** [Contact us](mailto:[email protected]) to upgrade.
|
**Cloud:** [Contact us](mailto:[email protected]) to upgrade.
|
||||||
|
|
||||||
**Self-hosted:** Requires an Enterprise license key. Set `LICENSE_KEY` in the API service environment — see [Self Hosting → SSO & licensing](/self-hosted/configuration/sso).
|
**Self-hosted:** Requires an Enterprise license key. Set `LICENSE_KEY` in the API service environment — see [Self Hosting → Enterprise & licensing](/self-hosted/enterprise).
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"@tailwindcss/typography": "0.5.19",
|
"@tailwindcss/typography": "0.5.19",
|
||||||
"@tailwindcss/vite": "4.1.18",
|
"@tailwindcss/vite": "4.1.18",
|
||||||
"astro": "7.0.9",
|
"astro": "7.0.9",
|
||||||
"blume": "1.1.2",
|
"blume": "1.2.0",
|
||||||
"mermaid": "^11.16.0",
|
"mermaid": "^11.16.0",
|
||||||
"tailwindcss": "4.1.18"
|
"tailwindcss": "4.1.18"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ const REDIRECTS = `# Generated by scripts/package-assets.mjs — do not edit.
|
|||||||
|
|
||||||
/docs/contributor-guides /docs/guides/contributing 301
|
/docs/contributor-guides /docs/guides/contributing 301
|
||||||
/docs/contributor-guides/* /docs/guides/contributing/:splat 301
|
/docs/contributor-guides/* /docs/guides/contributing/:splat 301
|
||||||
|
|
||||||
|
/docs/self-hosted/configuration/sso /docs/self-hosted/enterprise 301
|
||||||
${GUIDE_REDIRECTS}
|
${GUIDE_REDIRECTS}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { client } from '$lib/utils/posthog';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
export const load = ({ request }) => {
|
||||||
|
client.capture({
|
||||||
|
distinctId: request.headers.get('x-forwarded-for') || new Date().getTime().toString(),
|
||||||
|
event: 'classroomio.sh downloaded'
|
||||||
|
});
|
||||||
|
|
||||||
|
redirect(307, 'https://raw.githubusercontent.com/classroomio/classroomio/main/classroomio.sh');
|
||||||
|
};
|
||||||
Executable
+666
@@ -0,0 +1,666 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# ClassroomIO self-host lifecycle manager.
|
||||||
|
#
|
||||||
|
# Run with no arguments for an interactive menu, or pass a subcommand directly:
|
||||||
|
#
|
||||||
|
# ./classroomio.sh # interactive menu
|
||||||
|
# ./classroomio.sh install # first-time setup: fetch files, create .env, start
|
||||||
|
# ./classroomio.sh start # non-interactive start (env auto-setup + up -d)
|
||||||
|
# ./classroomio.sh stop|restart|upgrade|logs|backup
|
||||||
|
#
|
||||||
|
# The script is standalone: run it in an empty directory and `install` downloads
|
||||||
|
# docker-compose.images.yaml + .env.example from GitHub. Inside a repo checkout it
|
||||||
|
# uses the local files. It never overwrites an existing .env or compose file.
|
||||||
|
#
|
||||||
|
# The compose file tracks `main` (the stack topology); CIO_VERSION in .env pins the
|
||||||
|
# app images. Pin an exact release (e.g. 1.4.2) in production.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_NAME="classroomio"
|
||||||
|
ENV_FILE="${ROOT_DIR}/.env"
|
||||||
|
ENV_EXAMPLE_FILE="${ROOT_DIR}/.env.example"
|
||||||
|
BUILD_COMPOSE_FILE="${ROOT_DIR}/docker-compose.yaml"
|
||||||
|
IMAGES_COMPOSE_FILE="${ROOT_DIR}/docker-compose.images.yaml"
|
||||||
|
RAW_BASE_URL="https://raw.githubusercontent.com/classroomio/classroomio/main"
|
||||||
|
|
||||||
|
# Default to PULLING pre-built images (fast). Pass --build to build from source.
|
||||||
|
USE_IMAGES=true
|
||||||
|
COMPOSE_FILE="${IMAGES_COMPOSE_FILE}"
|
||||||
|
# --profile flags live in ONE variable so every command (start, stop, logs, backup)
|
||||||
|
# sees the same profile state. --no-minio empties it.
|
||||||
|
PROFILE_ARGS="--profile minio"
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage: ./classroomio.sh [options] [command] [args]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
install First-time setup: fetch compose file + .env.example (if missing),
|
||||||
|
create .env, generate secrets, pull images and start the stack.
|
||||||
|
start Start the stack (auto-generates missing secrets first).
|
||||||
|
stop Stop all containers (data volumes are kept).
|
||||||
|
restart Restart all containers.
|
||||||
|
upgrade Back up first, then pull newer images and restart.
|
||||||
|
logs [svc] Follow logs (all services, or one: api, dashboard, jobs, postgres, ...).
|
||||||
|
backup Dump Postgres and archive the MinIO volume into ./backups/.
|
||||||
|
|
||||||
|
Run without a command to get an interactive menu.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--build Use docker-compose.yaml and build images from source instead of pulling.
|
||||||
|
Requires a local repo checkout (git clone) — not available in a
|
||||||
|
standalone directory set up via 'install'.
|
||||||
|
--no-minio Exclude the bundled MinIO (requires external S3-compatible storage).
|
||||||
|
-h, --help Show this help message.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Compose CLI detection: `docker compose` (v2) or legacy `docker-compose`.
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
COMPOSE_CMD=""
|
||||||
|
|
||||||
|
detect_compose() {
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "Error: docker is not installed or not on PATH."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="docker-compose"
|
||||||
|
else
|
||||||
|
echo "Error: neither 'docker compose' nor 'docker-compose' is available."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# All compose invocations go through this so the env file, project name, compose
|
||||||
|
# file, and profiles are consistent everywhere. Word-splitting of COMPOSE_CMD and
|
||||||
|
# PROFILE_ARGS is intentional.
|
||||||
|
compose() {
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
${COMPOSE_CMD} --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ${PROFILE_ARGS} "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# .env helpers
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
get_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local line
|
||||||
|
line="$(grep -E "^${key}=" "${ENV_FILE}" | tail -n 1 || true)"
|
||||||
|
printf '%s' "${line#*=}"
|
||||||
|
}
|
||||||
|
|
||||||
|
upsert_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
local tmp_file
|
||||||
|
|
||||||
|
tmp_file="$(mktemp)"
|
||||||
|
awk -v key="${key}" -v value="${value}" '
|
||||||
|
BEGIN { updated = 0 }
|
||||||
|
$0 ~ ("^" key "=") {
|
||||||
|
if (updated == 0) {
|
||||||
|
print key "=" value;
|
||||||
|
updated = 1;
|
||||||
|
}
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
END {
|
||||||
|
if (updated == 0) {
|
||||||
|
print key "=" value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "${ENV_FILE}" > "${tmp_file}"
|
||||||
|
|
||||||
|
mv "${tmp_file}" "${ENV_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_insecure_token_value() {
|
||||||
|
local value="$1"
|
||||||
|
local normalized
|
||||||
|
|
||||||
|
normalized="$(printf '%s' "${value}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
|
||||||
|
if [[ "${normalized}" =~ ^\"(.*)\"$ ]]; then
|
||||||
|
normalized="${BASH_REMATCH[1]}"
|
||||||
|
elif [[ "${normalized}" =~ ^\'(.*)\'$ ]]; then
|
||||||
|
normalized="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${normalized}" in
|
||||||
|
"" | "replace-with-a-long-random-token" | "local-dev-api-key" | "changeme" | "replace-me")
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
replace-with-* | *local-dev* | *change-this* | *your-*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_secure_token() {
|
||||||
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
|
openssl rand -hex 32
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -r /dev/urandom ]]; then
|
||||||
|
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
||||||
|
echo
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Last resort only: no openssl, node, python3, or /dev/urandom. A timestamp-derived
|
||||||
|
# value is weak (guessable within the install's time window) but better than failing.
|
||||||
|
date +%s%N | shasum | awk '{ print $1 }'
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_secure_auth_tokens() {
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
touch "${ENV_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local private_server_key
|
||||||
|
private_server_key="$(get_env_value PRIVATE_SERVER_KEY)"
|
||||||
|
|
||||||
|
# PRIVATE_SERVER_KEY authenticates the dashboard's server-side calls to the API and must be
|
||||||
|
# identical in both services. Generate a strong value when it's empty or a known placeholder.
|
||||||
|
if is_insecure_token_value "${private_server_key}"; then
|
||||||
|
upsert_env_value PRIVATE_SERVER_KEY "$(generate_secure_token)"
|
||||||
|
echo "Generated secure PRIVATE_SERVER_KEY in .env"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_secure_betterauth_secret() {
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
touch "${ENV_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local secret
|
||||||
|
secret="$(get_env_value BETTER_AUTH_SECRET)"
|
||||||
|
|
||||||
|
# Only (re)generate when the value is empty or a known placeholder; never clobber
|
||||||
|
# a strong secret the user has set themselves.
|
||||||
|
if is_insecure_token_value "${secret}"; then
|
||||||
|
upsert_env_value BETTER_AUTH_SECRET "$(generate_secure_token)"
|
||||||
|
echo "Generated secure BETTER_AUTH_SECRET in .env"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_local_origin() {
|
||||||
|
local value="$1"
|
||||||
|
case "${value}" in
|
||||||
|
"" | *localhost* | *127.0.0.1* | *0.0.0.0*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Point browser-facing storage URLs at the public domain when DASHBOARD_ORIGIN is not
|
||||||
|
# localhost. MinIO media is served on :9000 — operators must reverse-proxy /media to it.
|
||||||
|
# `mode` is "bundled" (ensure_minio_env) or "external" (ensure_storage_env's --no-minio
|
||||||
|
# path). For bundled MinIO, OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL is script-owned, so we
|
||||||
|
# keep it in sync with DASHBOARD_ORIGIN on every run — otherwise changing DASHBOARD_ORIGIN
|
||||||
|
# after install would leave media links pointed at the old domain forever. For external
|
||||||
|
# storage it's operator-owned (their CDN/bucket URL), so we only fill in a default and
|
||||||
|
# never clobber an explicit value.
|
||||||
|
derive_public_storage_urls() {
|
||||||
|
local mode="$1"
|
||||||
|
local dashboard_origin public_endpoint media_base
|
||||||
|
dashboard_origin="$(get_env_value DASHBOARD_ORIGIN)"
|
||||||
|
public_endpoint="$(get_env_value OBJECT_STORAGE_PUBLIC_ENDPOINT)"
|
||||||
|
media_base="$(get_env_value OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL)"
|
||||||
|
|
||||||
|
if ! is_local_origin "${dashboard_origin}"; then
|
||||||
|
local origin="${dashboard_origin%/}"
|
||||||
|
local derived="${origin}/media"
|
||||||
|
# Public media is served from the public-download `media` bucket, so deriving its base
|
||||||
|
# URL from the domain is what fixes broken media links in served pages. We deliberately
|
||||||
|
# do NOT touch OBJECT_STORAGE_PUBLIC_ENDPOINT (the S3 API endpoint) — pointing it at the
|
||||||
|
# dashboard origin would break presigned URLs for the non-public videos/documents buckets.
|
||||||
|
if [[ "${mode}" == "bundled" ]]; then
|
||||||
|
if [[ "${media_base}" != "${derived}" ]]; then
|
||||||
|
upsert_env_value OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL "${derived}"
|
||||||
|
echo "Derived OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL from DASHBOARD_ORIGIN (${derived})."
|
||||||
|
echo " -> Ensure your reverse proxy routes ${origin}/media to MinIO (port 9000)."
|
||||||
|
echo " -> For presigned access to the videos/documents buckets behind a domain, expose"
|
||||||
|
echo " MinIO's S3 API on its own route and set OBJECT_STORAGE_PUBLIC_ENDPOINT explicitly."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if is_local_origin "${media_base}"; then
|
||||||
|
upsert_env_value OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL "${derived}"
|
||||||
|
echo "Derived OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL from DASHBOARD_ORIGIN (${derived})."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Local demo: ensure localhost defaults are present.
|
||||||
|
if [[ -z "${public_endpoint}" ]]; then
|
||||||
|
upsert_env_value OBJECT_STORAGE_PUBLIC_ENDPOINT "http://localhost:9000"
|
||||||
|
fi
|
||||||
|
if [[ -z "${media_base}" ]]; then
|
||||||
|
upsert_env_value OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL "http://localhost:9000/media"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_minio_env() {
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
touch "${ENV_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local minio_password
|
||||||
|
minio_password="$(get_env_value MINIO_ROOT_PASSWORD)"
|
||||||
|
|
||||||
|
# First provision: randomize credentials whenever the MinIO password is still empty or the
|
||||||
|
# well-known `minioadmin` placeholder shipped in .env.example. (Previously this gated on
|
||||||
|
# OBJECT_STORAGE_ENDPOINT being empty, but .env.example ships it non-empty — so on a normal
|
||||||
|
# copy-the-example first run the randomization was always skipped and minioadmin/minioadmin
|
||||||
|
# survived.) MinIO uses its root credentials as the S3 access key / secret.
|
||||||
|
if [[ -z "${minio_password}" || "${minio_password}" =~ ^[[:space:]]*$ || "${minio_password}" == "minioadmin" ]]; then
|
||||||
|
local minio_user
|
||||||
|
minio_user="cio-$(generate_secure_token | cut -c1-16)"
|
||||||
|
minio_password="$(generate_secure_token)"
|
||||||
|
|
||||||
|
upsert_env_value MINIO_ROOT_USER "${minio_user}"
|
||||||
|
upsert_env_value MINIO_ROOT_PASSWORD "${minio_password}"
|
||||||
|
upsert_env_value OBJECT_STORAGE_ENDPOINT "http://minio:9000"
|
||||||
|
upsert_env_value OBJECT_STORAGE_ACCESS_KEY_ID "${minio_user}"
|
||||||
|
upsert_env_value OBJECT_STORAGE_SECRET_ACCESS_KEY "${minio_password}"
|
||||||
|
upsert_env_value OBJECT_STORAGE_FORCE_PATH_STYLE "true"
|
||||||
|
echo "Provisioned MinIO with randomized credentials in .env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
derive_public_storage_urls "bundled"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Shared checks
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
# When MinIO is excluded the user must supply external object storage, or uploads/media
|
||||||
|
# silently break. Fail fast instead. The endpoint must be set AND not still point at the
|
||||||
|
# bundled MinIO host (which won't be running with --no-minio).
|
||||||
|
ensure_storage_env() {
|
||||||
|
if [[ "${PROFILE_ARGS}" == *"minio"* ]]; then
|
||||||
|
ensure_minio_env
|
||||||
|
else
|
||||||
|
local external_endpoint
|
||||||
|
external_endpoint="$(get_env_value OBJECT_STORAGE_ENDPOINT)"
|
||||||
|
if [[ -z "${external_endpoint}" || "${external_endpoint}" == *"minio:9000"* ]]; then
|
||||||
|
echo "Error: --no-minio was passed but no external object storage is configured."
|
||||||
|
echo "OBJECT_STORAGE_ENDPOINT is unset or still points at the bundled MinIO (minio:9000)."
|
||||||
|
echo "Configure an external S3-compatible store via OBJECT_STORAGE_* in .env,"
|
||||||
|
echo "or drop --no-minio to use the bundled MinIO."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
derive_public_storage_urls "external"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
warn_if_unpinned_version() {
|
||||||
|
local version
|
||||||
|
version="$(get_env_value CIO_VERSION)"
|
||||||
|
if [[ -z "${version}" || "${version}" == "latest" ]]; then
|
||||||
|
echo
|
||||||
|
echo "WARNING: CIO_VERSION is '${version:-unset}' — you are tracking the rolling 'latest' tag."
|
||||||
|
echo " 'latest' moves on every merge to main and may include unreleased code."
|
||||||
|
echo " For production, pin an exact release in .env, e.g.: CIO_VERSION=1.4.2"
|
||||||
|
echo " Available tags: https://hub.docker.com/r/classroomio/api/tags"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# `docker compose up -d` returns once containers are *started*, not *healthy*. On a fresh
|
||||||
|
# deployment the API runs DB migrations before it listens, so the endpoints aren't reachable
|
||||||
|
# immediately. Poll with a retry instead of a single curl — and never let a not-yet-ready
|
||||||
|
# check abort the script (set -euo pipefail is active), so first runs end cleanly.
|
||||||
|
check_endpoint() {
|
||||||
|
local name="$1" url="$2" curl_flags="$3" attempts="$4"
|
||||||
|
local i=0
|
||||||
|
while ((i < attempts)); do
|
||||||
|
if curl "${curl_flags}" --max-time 5 "${url}" >/dev/null 2>&1; then
|
||||||
|
echo "${name} is reachable on ${url}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "Note: ${name} is not reachable yet at ${url} (waited $((attempts * 5))s)."
|
||||||
|
echo " It may still be starting — the first run migrates the database. Follow logs with:"
|
||||||
|
echo " ./classroomio.sh logs"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_endpoints() {
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
echo
|
||||||
|
echo "Waiting for services to come up (first run can take a minute while the API migrates)..."
|
||||||
|
check_endpoint "API" "http://localhost:3081/" "-fsS" 24
|
||||||
|
check_endpoint "Dashboard" "http://localhost:3082/" "-fsSI" 12
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo "curl not found, skipped endpoint checks."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_if_missing() {
|
||||||
|
local file="$1" url="$2"
|
||||||
|
if [[ -f "${file}" ]]; then
|
||||||
|
echo "Keeping existing $(basename "${file}") (not overwritten)."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "Downloading $(basename "${file}")..."
|
||||||
|
# Download to a temp name and move into place so a failed transfer can never
|
||||||
|
# leave a partial file that a re-run would mistake for a kept local copy.
|
||||||
|
local tmp="${file}.download"
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL --retry 3 --retry-delay 2 -o "${tmp}" "${url}" || { rm -f "${tmp}"; echo "Error: failed to download ${url}"; exit 1; }
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -q -O "${tmp}" "${url}" || { rm -f "${tmp}"; echo "Error: failed to download ${url}"; exit 1; }
|
||||||
|
else
|
||||||
|
echo "Error: need curl or wget to download $(basename "${file}")."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mv "${tmp}" "${file}"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_env_and_secrets() {
|
||||||
|
ensure_secure_auth_tokens
|
||||||
|
ensure_secure_betterauth_secret
|
||||||
|
ensure_storage_env
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Commands
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
cmd_install() {
|
||||||
|
echo "Installing ClassroomIO..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ "${USE_IMAGES}" != "true" && ! -f "${BUILD_COMPOSE_FILE}" ]]; then
|
||||||
|
echo "Error: --build install requires a local repo checkout (docker-compose.yaml not found)."
|
||||||
|
echo "Clone the repository instead, or run 'install' without --build to pull pre-built images."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${USE_IMAGES}" == "true" ]]; then
|
||||||
|
# The compose file tracks main (stack topology only — CIO_VERSION pins the app
|
||||||
|
# images). Never overwritten; refresh manually if you need a newer topology:
|
||||||
|
# curl -fsSLO ${RAW_BASE_URL}/docker-compose.images.yaml
|
||||||
|
fetch_if_missing "${IMAGES_COMPOSE_FILE}" "${RAW_BASE_URL}/docker-compose.images.yaml"
|
||||||
|
fi
|
||||||
|
fetch_if_missing "${ENV_EXAMPLE_FILE}" "${RAW_BASE_URL}/.env.example"
|
||||||
|
|
||||||
|
if [[ -f "${ENV_FILE}" ]]; then
|
||||||
|
echo "Keeping existing .env (not overwritten)."
|
||||||
|
else
|
||||||
|
cp "${ENV_EXAMPLE_FILE}" "${ENV_FILE}"
|
||||||
|
echo "Created .env from .env.example"
|
||||||
|
fi
|
||||||
|
|
||||||
|
prepare_env_and_secrets
|
||||||
|
warn_if_unpinned_version
|
||||||
|
|
||||||
|
echo "Review these values in .env before going to production:"
|
||||||
|
echo " DASHBOARD_ORIGIN (your public https:// dashboard URL)"
|
||||||
|
echo " SMTP_* (outgoing email — invites, notifications)"
|
||||||
|
echo " CIO_VERSION (pin an exact release for production)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
cmd_start
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_start() {
|
||||||
|
prepare_env_and_secrets
|
||||||
|
warn_if_unpinned_version
|
||||||
|
|
||||||
|
echo "Starting ClassroomIO..."
|
||||||
|
if [[ "${PROFILE_ARGS}" == *"minio"* ]]; then
|
||||||
|
echo "Including MinIO (object storage, default)..."
|
||||||
|
fi
|
||||||
|
if [[ "${USE_IMAGES}" == "true" ]]; then
|
||||||
|
echo "Using pre-built images from Docker Hub (CIO_VERSION=$(get_env_value CIO_VERSION))..."
|
||||||
|
compose pull
|
||||||
|
compose up -d
|
||||||
|
else
|
||||||
|
compose up --build -d
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Current service status:"
|
||||||
|
compose ps
|
||||||
|
|
||||||
|
wait_for_endpoints
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done. Full stack is running."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_stop() {
|
||||||
|
echo "Stopping ClassroomIO (data volumes are kept)..."
|
||||||
|
compose stop
|
||||||
|
echo "Stopped. Start again with: ./classroomio.sh start"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_restart() {
|
||||||
|
prepare_env_and_secrets
|
||||||
|
echo "Restarting ClassroomIO..."
|
||||||
|
# `compose restart` only bounces the container *process* — it never re-reads .env, so
|
||||||
|
# edits to DASHBOARD_ORIGIN/SMTP_*/LICENSE_KEY/etc. were silently ignored. Recreate
|
||||||
|
# instead: --force-recreate guarantees an actual restart even when config is
|
||||||
|
# unchanged, without pulling new images (that's what `upgrade` is for).
|
||||||
|
compose up -d --force-recreate
|
||||||
|
compose ps
|
||||||
|
wait_for_endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() {
|
||||||
|
compose logs -f --tail=100 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_backup() {
|
||||||
|
local ts backup_dir db_user db_name
|
||||||
|
ts="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
backup_dir="${ROOT_DIR}/backups"
|
||||||
|
mkdir -p "${backup_dir}"
|
||||||
|
|
||||||
|
db_user="$(get_env_value POSTGRES_USER)"
|
||||||
|
db_user="${db_user:-postgres}"
|
||||||
|
db_name="$(get_env_value POSTGRES_DB)"
|
||||||
|
db_name="${db_name:-classroomio}"
|
||||||
|
|
||||||
|
echo "Backing up Postgres database '${db_name}'..."
|
||||||
|
local db_file="${backup_dir}/classroomio-db-${ts}.sql.gz"
|
||||||
|
# pg_dump failure must fail the backup (and abort an upgrade) — don't let gzip's
|
||||||
|
# exit code mask it.
|
||||||
|
if ! compose exec -T postgres pg_dump -U "${db_user}" "${db_name}" | gzip > "${db_file}"; then
|
||||||
|
rm -f "${db_file}"
|
||||||
|
echo "Error: Postgres backup failed. Is the stack running? (./classroomio.sh start)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo " -> ${db_file}"
|
||||||
|
|
||||||
|
# MinIO volume name is derived from the compose project name — same variable the
|
||||||
|
# -p flag uses, so a project rename can't silently desync the backup target.
|
||||||
|
local minio_volume="${PROJECT_NAME}_minio-data"
|
||||||
|
if docker volume inspect "${minio_volume}" >/dev/null 2>&1; then
|
||||||
|
echo "Backing up MinIO volume '${minio_volume}'..."
|
||||||
|
local minio_file="classroomio-minio-${ts}.tar.gz"
|
||||||
|
# Git Bash (MSYS) rewrites container paths like /data into Windows paths; disable
|
||||||
|
# conversion for this one command and pre-convert the host dir with cygpath.
|
||||||
|
# On Linux/macOS cygpath doesn't exist and the fallback keeps the path as-is.
|
||||||
|
local backup_dir_host
|
||||||
|
backup_dir_host="$(cygpath -w "${backup_dir}" 2>/dev/null || printf '%s' "${backup_dir}")"
|
||||||
|
if ! MSYS_NO_PATHCONV=1 docker run --rm \
|
||||||
|
-v "${minio_volume}:/data:ro" \
|
||||||
|
-v "${backup_dir_host}:/backup" \
|
||||||
|
alpine tar czf "/backup/${minio_file}" -C /data .; then
|
||||||
|
echo "Error: MinIO volume backup failed."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo " -> ${backup_dir}/${minio_file}"
|
||||||
|
else
|
||||||
|
# Not an error: --no-minio installs use external storage and have no local volume.
|
||||||
|
echo "MinIO volume '${minio_volume}' not found — skipping object-storage backup."
|
||||||
|
echo " (Using external S3/R2? Back that up with your provider's tools.)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Backup complete: ${backup_dir}"
|
||||||
|
echo "Copy backups off this machine — a server failure takes local backups with it."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_upgrade() {
|
||||||
|
prepare_env_and_secrets
|
||||||
|
echo "Upgrading ClassroomIO..."
|
||||||
|
echo
|
||||||
|
echo "Step 1/3: backing up before touching images..."
|
||||||
|
cmd_backup
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [[ "${USE_IMAGES}" == "true" ]]; then
|
||||||
|
# CIO_VERSION only selects a published image tag — meaningless for --build,
|
||||||
|
# which always builds whatever is in the local checkout.
|
||||||
|
local current_version
|
||||||
|
current_version="$(get_env_value CIO_VERSION)"
|
||||||
|
echo "Step 2/3: current CIO_VERSION is '${current_version:-unset}'."
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
read -r -p "Enter a new version tag to pin (or press Enter to keep the current one): " new_version
|
||||||
|
if [[ -n "${new_version}" ]]; then
|
||||||
|
upsert_env_value CIO_VERSION "${new_version}"
|
||||||
|
echo "Set CIO_VERSION=${new_version} in .env"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
warn_if_unpinned_version
|
||||||
|
else
|
||||||
|
echo "Step 2/3: building from source — CIO_VERSION doesn't apply."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${USE_IMAGES}" == "true" ]]; then
|
||||||
|
echo "Step 3/3: pulling images and restarting..."
|
||||||
|
compose pull
|
||||||
|
compose up -d
|
||||||
|
else
|
||||||
|
echo "Step 3/3: rebuilding from source and restarting..."
|
||||||
|
compose up --build -d
|
||||||
|
fi
|
||||||
|
compose ps
|
||||||
|
wait_for_endpoints
|
||||||
|
echo
|
||||||
|
echo "Upgrade complete."
|
||||||
|
if [[ "${USE_IMAGES}" == "true" ]]; then
|
||||||
|
echo "Roll back by setting CIO_VERSION back in .env and running: ./classroomio.sh upgrade"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_menu() {
|
||||||
|
echo "ClassroomIO self-host manager"
|
||||||
|
echo
|
||||||
|
echo " 1) Install (first-time setup: fetch files, create .env, start)"
|
||||||
|
echo " 2) Start"
|
||||||
|
echo " 3) Stop"
|
||||||
|
echo " 4) Restart"
|
||||||
|
echo " 5) Upgrade (backs up first, then pulls new images)"
|
||||||
|
echo " 6) View logs"
|
||||||
|
echo " 7) Backup (Postgres dump + MinIO volume archive)"
|
||||||
|
echo " q) Quit"
|
||||||
|
echo
|
||||||
|
read -r -p "Pick an option: " choice
|
||||||
|
case "${choice}" in
|
||||||
|
1) cmd_install ;;
|
||||||
|
2) cmd_start ;;
|
||||||
|
3) cmd_stop ;;
|
||||||
|
4) cmd_restart ;;
|
||||||
|
5) cmd_upgrade ;;
|
||||||
|
6) cmd_logs ;;
|
||||||
|
7) cmd_backup ;;
|
||||||
|
q|Q) exit 0 ;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: ${choice}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Argument parsing
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Options may appear before or after the command: `./classroomio.sh start --build`
|
||||||
|
# and `./classroomio.sh --build start` both work.
|
||||||
|
COMMAND=""
|
||||||
|
COMMAND_ARGS=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--build)
|
||||||
|
USE_IMAGES=false
|
||||||
|
COMPOSE_FILE="${BUILD_COMPOSE_FILE}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-minio)
|
||||||
|
PROFILE_ARGS=""
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
print_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
install|start|stop|restart|upgrade|logs|backup)
|
||||||
|
if [[ -n "${COMMAND}" ]]; then
|
||||||
|
echo "Unexpected extra command: $1 (already running '${COMMAND}')"
|
||||||
|
print_usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
COMMAND="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ "${COMMAND}" == "logs" ]]; then
|
||||||
|
COMMAND_ARGS+=("$1")
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
print_usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
detect_compose
|
||||||
|
|
||||||
|
case "${COMMAND}" in
|
||||||
|
"") show_menu ;;
|
||||||
|
install) cmd_install ;;
|
||||||
|
start) cmd_start ;;
|
||||||
|
stop) cmd_stop ;;
|
||||||
|
restart) cmd_restart ;;
|
||||||
|
upgrade) cmd_upgrade ;;
|
||||||
|
logs) cmd_logs "${COMMAND_ARGS[@]+"${COMMAND_ARGS[@]}"}" ;;
|
||||||
|
backup) cmd_backup ;;
|
||||||
|
esac
|
||||||
@@ -132,8 +132,8 @@ services:
|
|||||||
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY:-}
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY:-}
|
||||||
OBJECT_STORAGE_FORCE_PATH_STYLE: ${OBJECT_STORAGE_FORCE_PATH_STYLE:-true}
|
OBJECT_STORAGE_FORCE_PATH_STYLE: ${OBJECT_STORAGE_FORCE_PATH_STYLE:-true}
|
||||||
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL: ${OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL:-}
|
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL: ${OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL:-}
|
||||||
# Auth secrets — required. Set strong values in .env (the pull path has no script to
|
# Auth secrets — required. ./classroomio.sh generates them into .env; if you run
|
||||||
# generate them); the stack refuses to start otherwise.
|
# compose directly, set strong values yourself — the stack refuses to start otherwise.
|
||||||
PRIVATE_SERVER_KEY: ${PRIVATE_SERVER_KEY:?set a strong PRIVATE_SERVER_KEY in .env}
|
PRIVATE_SERVER_KEY: ${PRIVATE_SERVER_KEY:?set a strong PRIVATE_SERVER_KEY in .env}
|
||||||
DASHBOARD_ORIGIN: ${DASHBOARD_ORIGIN:-}
|
DASHBOARD_ORIGIN: ${DASHBOARD_ORIGIN:-}
|
||||||
LICENSE_KEY: ${LICENSE_KEY:-}
|
LICENSE_KEY: ${LICENSE_KEY:-}
|
||||||
|
|||||||
+1
-1
@@ -126,7 +126,7 @@ services:
|
|||||||
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY:-}
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY:-}
|
||||||
OBJECT_STORAGE_FORCE_PATH_STYLE: ${OBJECT_STORAGE_FORCE_PATH_STYLE:-true}
|
OBJECT_STORAGE_FORCE_PATH_STYLE: ${OBJECT_STORAGE_FORCE_PATH_STYLE:-true}
|
||||||
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL: ${OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL:-}
|
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL: ${OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL:-}
|
||||||
# Auth secrets — required. Generated by run-docker-full-stack.sh; set strong values
|
# Auth secrets — required. Generated by classroomio.sh; set strong values
|
||||||
# in .env if you start the stack with `docker compose` directly.
|
# in .env if you start the stack with `docker compose` directly.
|
||||||
PRIVATE_SERVER_KEY: ${PRIVATE_SERVER_KEY:?set a strong PRIVATE_SERVER_KEY in .env}
|
PRIVATE_SERVER_KEY: ${PRIVATE_SERVER_KEY:?set a strong PRIVATE_SERVER_KEY in .env}
|
||||||
DASHBOARD_ORIGIN: ${DASHBOARD_ORIGIN:-}
|
DASHBOARD_ORIGIN: ${DASHBOARD_ORIGIN:-}
|
||||||
|
|||||||
+64
-42
@@ -12,20 +12,20 @@ git clone https://github.com/classroomio/classroomio.git
|
|||||||
cd classroomio
|
cd classroomio
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env — set your dashboard domain and secrets
|
# Edit .env — set your dashboard domain and secrets
|
||||||
./run-docker-full-stack.sh
|
./classroomio.sh start
|
||||||
```
|
```
|
||||||
|
|
||||||
The script builds and starts: **postgres**, **redis**, **minio** (object storage), **api** (`localhost:3081`), **dashboard** (`localhost:3082`), and the **jobs** worker (background processing). Database schema setup runs automatically inside the `api` container on startup (see [`docker/entrypoint-api.sh`](../entrypoint-api.sh); skip with `SKIP_DB_SETUP=true`).
|
The script pulls and starts: **postgres**, **redis**, **minio** (object storage), **api** (`localhost:3081`), **dashboard** (`localhost:3082`), and the **jobs** worker (background processing). Pass `--build` to build the application images from this checkout instead of pulling published ones. Database schema setup runs automatically inside the `api` container on startup (see [`docker/entrypoint-api.sh`](../entrypoint-api.sh); skip with `SKIP_DB_SETUP=true`).
|
||||||
|
|
||||||
It auto-generates secure values for `PRIVATE_SERVER_KEY` and `BETTER_AUTH_SECRET` when they are missing or left at placeholders, and randomizes MinIO credentials on first provision.
|
It auto-generates secure values for `PRIVATE_SERVER_KEY` and `BETTER_AUTH_SECRET` when they are missing or left at placeholders, and randomizes MinIO credentials on first provision.
|
||||||
|
|
||||||
> **The jobs worker is required, not optional.** Without it, video processing, captions/transcription, AI course generation, and most emails are enqueued in Redis but never run — uploads stay stuck "processing" with no error. See [Background Jobs Worker](#background-jobs-worker) below.
|
> **The jobs worker is required, not optional.** Without it, video processing, captions/transcription, AI course generation, and most emails are enqueued in Redis but never run: uploads stay stuck "processing" with no error. See [Background Jobs Worker](#background-jobs-worker) below.
|
||||||
|
|
||||||
## Run from Pre-built Images (no build)
|
## Run from Pre-built Images (no build)
|
||||||
|
|
||||||
The Quick Start builds images from source — that needs the full repo and ~8 GB RAM for the
|
The Quick Start already pulls the published `classroomio/{api,dashboard,jobs}` images by default.
|
||||||
dashboard build. Most self-hosters should instead **pull** the published `classroomio/{api,dashboard,jobs}`
|
To skip cloning the full repository, you only need
|
||||||
images. You only need [`docker-compose.images.yaml`](../../docker-compose.images.yaml) and a `.env`:
|
[`docker-compose.images.yaml`](../../docker-compose.images.yaml) and a `.env`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Get just the compose file + env template (no full clone required)
|
# 1. Get just the compose file + env template (no full clone required)
|
||||||
@@ -37,10 +37,18 @@ CIO_VERSION=1.4.2 docker compose -f docker-compose.images.yaml --env-file .env p
|
|||||||
CIO_VERSION=1.4.2 docker compose -f docker-compose.images.yaml --env-file .env up -d
|
CIO_VERSION=1.4.2 docker compose -f docker-compose.images.yaml --env-file .env up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
From a full clone you can also use the helper: `./run-docker-full-stack.sh --images`.
|
Easier: let the lifecycle script do all of the above (it also auto-generates secrets). It works in
|
||||||
|
an empty directory: `install` downloads the compose file and `.env.example` for you:
|
||||||
|
|
||||||
> Secrets aren't auto-generated in this path (the helper script does that). Set `BETTER_AUTH_SECRET`
|
```bash
|
||||||
> and `PRIVATE_SERVER_KEY` to strong random values yourself.
|
curl -fsSLO https://classroomio.com/classroomio.sh
|
||||||
|
chmod +x classroomio.sh
|
||||||
|
./classroomio.sh install
|
||||||
|
```
|
||||||
|
|
||||||
|
> If you run the raw `docker compose` commands instead of the script, secrets aren't
|
||||||
|
> auto-generated. Set `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` to strong random values
|
||||||
|
> yourself.
|
||||||
|
|
||||||
## Versioning & Upgrades
|
## Versioning & Upgrades
|
||||||
|
|
||||||
@@ -65,30 +73,40 @@ CIO_VERSION=1.5.0 docker compose -f docker-compose.images.yaml --env-file .env u
|
|||||||
|
|
||||||
Schema migrations run automatically on `api` startup (`db:setup`), so upgrades need no manual DB
|
Schema migrations run automatically on `api` startup (`db:setup`), so upgrades need no manual DB
|
||||||
step. If a release consolidates ("squashes") older migrations, the startup baseline detects an
|
step. If a release consolidates ("squashes") older migrations, the startup baseline detects an
|
||||||
existing instance and adopts the new baseline as already-applied — your database is upgraded in
|
existing instance and adopts the new baseline as already-applied, and your database is upgraded in
|
||||||
place without re-running schema that already exists.
|
place without re-running schema that already exists.
|
||||||
|
|
||||||
(Maintainers: every published image is boot-smoke-tested in CI before it ships — see
|
(Maintainers: every published image is boot-smoke-tested in CI before it ships. See
|
||||||
[`PUBLISHING_IMAGES.md`](PUBLISHING_IMAGES.md).)
|
[`PUBLISHING_IMAGES.md`](PUBLISHING_IMAGES.md).)
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
All Docker services read from a single root `.env` file. See [`.env.example`](../../.env.example) for the full annotated template with required/optional grouping.
|
All Docker services read from a single root `.env` file. See [`.env.example`](../../.env.example) for the full annotated template with required/optional grouping.
|
||||||
|
|
||||||
Key points:
|
Two variables actually need your attention: `PUBLIC_IS_SELFHOSTED=true` and `DASHBOARD_ORIGIN`,
|
||||||
|
which you should set to your real domain in production (it also drives public media URLs; see
|
||||||
|
[Production Object Storage](#production-object-storage)). Everything else below has a sane
|
||||||
|
default.
|
||||||
|
|
||||||
- **Required:** `PUBLIC_IS_SELFHOSTED=true`, `DASHBOARD_ORIGIN` (set this to your real domain in production — it also drives public media URLs, see [Production Object Storage](#production-object-storage)).
|
Routing and auth are handled for you. Browser dashboard calls go to the dashboard origin and get
|
||||||
- **API routing:** Browser dashboard calls go to the dashboard origin and are proxied to `PRIVATE_SERVER_URL` (default: `http://api:3081`). You do not need API/dashboard cookie-domain matching for normal dashboard auth.
|
proxied to `PRIVATE_SERVER_URL` (default `http://api:3081`), so normal dashboard auth needs no
|
||||||
- **Direct API access (optional):** Set `PUBLIC_SERVER_URL` and `TRUSTED_ORIGINS` only if browsers or third-party clients need to call the API origin directly.
|
cookie-domain matching between the API and dashboard: the proxy makes it first-party, and Better
|
||||||
- **CSP (runtime):** `ALLOWED_EXTERNAL_DOMAINS` (overrides all) or per-directive: `CSP_SCRIPT_SRC_DOMAINS`, `CSP_STYLE_SRC_DOMAINS`, `CSP_CONNECT_SRC_DOMAINS`, `CSP_FRAME_SRC_DOMAINS`, `CSP_FONT_SRC_DOMAINS`, `CSP_MEDIA_SRC_DOMAINS`. These are read at container startup — no image rebuild needed. The API does not need to be added for normal dashboard calls.
|
Auth sets host-only cookies. Only set `PUBLIC_SERVER_URL` and `TRUSTED_ORIGINS` if a browser or
|
||||||
- **Auth cookies:** No cookie-domain env is needed. The dashboard proxy makes auth first-party and Better Auth sets host-only dashboard cookies.
|
third-party client needs to call the API directly. CSP is read at container startup rather than
|
||||||
- **Auto-generated:** `PRIVATE_SERVER_KEY`, `BETTER_AUTH_SECRET` (by `./run-docker-full-stack.sh`). A strong value you set yourself is never overwritten.
|
baked into the image, so `ALLOWED_EXTERNAL_DOMAINS` (or the per-directive `CSP_SCRIPT_SRC_DOMAINS`,
|
||||||
- **Auto-configured:** All `MINIO_*` / `OBJECT_STORAGE_*` vars (by the startup script, with randomized MinIO credentials).
|
`CSP_STYLE_SRC_DOMAINS`, `CSP_CONNECT_SRC_DOMAINS`, `CSP_FRAME_SRC_DOMAINS`, `CSP_FONT_SRC_DOMAINS`,
|
||||||
- **Email (effectively required):** SMTP (or Zoho). See [Email](#email) — without it, signup verification, password reset, and invites do not send.
|
`CSP_MEDIA_SRC_DOMAINS` vars) take effect after `./classroomio.sh restart` (no image rebuild
|
||||||
- **Optional:** Google OAuth, Unsplash, `LICENSE_KEY` (enterprise).
|
needed), and none of it needs to include the API for normal dashboard calls.
|
||||||
- **Optional — upload limits:** `UPLOAD_MAX_*_MB` vars (documents, images, videos, assignment files, etc.). See [`.env.example`](../../.env.example). Passed through to both `api` and `dashboard` containers; unset values use built-in defaults. Raise your reverse-proxy body-size limit too if you increase these.
|
|
||||||
|
|
||||||
`PRIVATE_SERVER_KEY` must be the same value in both API and dashboard — the script ensures this.
|
`PRIVATE_SERVER_KEY` and `BETTER_AUTH_SECRET` are generated for you by `./classroomio.sh` if you
|
||||||
|
leave them blank, and a value you set yourself is never overwritten; the script also ensures
|
||||||
|
`PRIVATE_SERVER_KEY` matches between API and dashboard. The same goes for the `MINIO_*` /
|
||||||
|
`OBJECT_STORAGE_*` vars: the startup script configures them with randomized credentials. Email is
|
||||||
|
effectively required: without SMTP (or Zoho) configured, signup verification, password resets, and
|
||||||
|
invites silently don't send (see [Email](#email)). Google OAuth, Unsplash, and `LICENSE_KEY` stay
|
||||||
|
off until you set them. If you raise the `UPLOAD_MAX_*_MB` vars (documents, images, videos,
|
||||||
|
assignment files; full list in [`.env.example`](../../.env.example)), set the same values on both
|
||||||
|
`api` and `dashboard`, and raise your reverse-proxy body-size limit to match.
|
||||||
|
|
||||||
## Verify
|
## Verify
|
||||||
|
|
||||||
@@ -109,7 +127,7 @@ docker exec cio-api pnpm --filter @cio/db db:setup:seed
|
|||||||
|
|
||||||
## Command Reference
|
## Command Reference
|
||||||
|
|
||||||
All commands use this prefix — abbreviated as `dc` below:
|
All commands use this prefix, abbreviated as `dc` below:
|
||||||
|
|
||||||
```
|
```
|
||||||
dc = docker compose --env-file .env -p classroomio -f docker-compose.yaml
|
dc = docker compose --env-file .env -p classroomio -f docker-compose.yaml
|
||||||
@@ -117,14 +135,18 @@ dc = docker compose --env-file .env -p classroomio -f docker-compose.yaml
|
|||||||
|
|
||||||
| Task | Command |
|
| Task | Command |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| Full stack (pulls pre-built images, with MinIO) | `./run-docker-full-stack.sh` |
|
| Interactive lifecycle menu (install/start/stop/upgrade/logs/backup) | `./classroomio.sh` |
|
||||||
| Build from source instead of pulling | `./run-docker-full-stack.sh --build` |
|
| Full stack (pulls pre-built images, with MinIO) | `./classroomio.sh start` |
|
||||||
| Exclude MinIO | `./run-docker-full-stack.sh --no-minio` |
|
| Build from source instead of pulling | `./classroomio.sh start --build` |
|
||||||
|
| Exclude MinIO | `./classroomio.sh start --no-minio` |
|
||||||
|
| Back up Postgres + MinIO volume | `./classroomio.sh backup` |
|
||||||
|
| Upgrade published images (backs up first) | `./classroomio.sh upgrade` |
|
||||||
|
| Upgrade a source build (backs up first, then rebuilds) | `./classroomio.sh upgrade --build` |
|
||||||
| API-only smoke test | `dc up --build -d postgres redis api` |
|
| API-only smoke test | `dc up --build -d postgres redis api` |
|
||||||
| Service status | `dc ps` |
|
| Service status | `dc ps` |
|
||||||
| Stream logs | `dc logs -f api dashboard` |
|
| Stream logs | `dc logs -f api dashboard` |
|
||||||
| Rebuild one service | `dc up -d --build api` |
|
| Rebuild one service | `dc up -d --build api` |
|
||||||
| Restart (env change, no rebuild) | `dc restart api dashboard` |
|
| Restart (recreates containers, applies .env changes) | `./classroomio.sh restart` |
|
||||||
| Stop | `dc down` |
|
| Stop | `dc down` |
|
||||||
| Stop + delete volumes | `dc down -v` |
|
| Stop + delete volumes | `dc down -v` |
|
||||||
|
|
||||||
@@ -177,11 +199,11 @@ Data is preserved in the `minio-data` volume.
|
|||||||
The `jobs` service (`cio-jobs`) is a BullMQ worker that processes everything ClassroomIO does
|
The `jobs` service (`cio-jobs`) is a BullMQ worker that processes everything ClassroomIO does
|
||||||
asynchronously:
|
asynchronously:
|
||||||
|
|
||||||
- **Video processing** — probe, thumbnails, audio extraction, transcoding
|
- **Video processing:** probe, thumbnails, audio extraction, transcoding
|
||||||
- **Transcription / captions** — via OpenAI Whisper (needs `OPENAI_API_KEY`)
|
- **Transcription / captions:** via OpenAI Whisper (needs `OPENAI_API_KEY`)
|
||||||
- **AI course generation** — the "generate a course with AI" agent
|
- **AI course generation:** the "generate a course with AI" agent
|
||||||
- **Emails** — invites, notifications, and other non-auth emails
|
- **Emails:** invites, notifications, and other non-auth emails
|
||||||
- **Maintenance** — periodic cleanup
|
- **Maintenance:** periodic cleanup
|
||||||
|
|
||||||
It shares Redis and the database with the API. It is started automatically by the full-stack
|
It shares Redis and the database with the API. It is started automatically by the full-stack
|
||||||
script. If it is not running, jobs accumulate in Redis and silently never complete.
|
script. If it is not running, jobs accumulate in Redis and silently never complete.
|
||||||
@@ -211,7 +233,7 @@ The bundled MinIO is fine for a single host. Two things matter for a real deploy
|
|||||||
1. **Public media URLs must point at your domain, not `localhost`.** Media URLs are embedded in
|
1. **Public media URLs must point at your domain, not `localhost`.** Media URLs are embedded in
|
||||||
pages served to browsers, so `http://localhost:9000/...` breaks for every remote visitor. The
|
pages served to browsers, so `http://localhost:9000/...` breaks for every remote visitor. The
|
||||||
startup script derives `OBJECT_STORAGE_PUBLIC_ENDPOINT` and
|
startup script derives `OBJECT_STORAGE_PUBLIC_ENDPOINT` and
|
||||||
`OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` from `DASHBOARD_ORIGIN` when it is a real domain — make
|
`OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` from `DASHBOARD_ORIGIN` when it is a real domain. Make
|
||||||
sure your reverse proxy routes `<your-domain>/media` to MinIO on port 9000 (or set those two
|
sure your reverse proxy routes `<your-domain>/media` to MinIO on port 9000 (or set those two
|
||||||
variables explicitly).
|
variables explicitly).
|
||||||
2. **External S3 / Cloudflare R2 instead of MinIO.** Run with `--no-minio` and set the
|
2. **External S3 / Cloudflare R2 instead of MinIO.** Run with `--no-minio` and set the
|
||||||
@@ -222,7 +244,7 @@ The bundled MinIO is fine for a single host. Two things matter for a real deploy
|
|||||||
## Backups & Persistence
|
## Backups & Persistence
|
||||||
|
|
||||||
All state lives in named Docker volumes: `postgres-data`, `redis-data`, `minio-data`
|
All state lives in named Docker volumes: `postgres-data`, `redis-data`, `minio-data`
|
||||||
(see `docker-compose.yaml`). **`docker compose ... down -v` deletes them — and all your
|
(see `docker-compose.yaml`). **`docker compose ... down -v` deletes them, and all your
|
||||||
data.** Back up regularly:
|
data.** Back up regularly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -240,7 +262,7 @@ Restore the database into a fresh stack with `psql`/`pg_restore` against `cio-po
|
|||||||
API replicas**, set `SKIP_DB_SETUP=true` on all but one to avoid concurrent migration races, and
|
API replicas**, set `SKIP_DB_SETUP=true` on all but one to avoid concurrent migration races, and
|
||||||
run setup once out-of-band. The worker queues can also be scaled independently by overriding the
|
run setup once out-of-band. The worker queues can also be scaled independently by overriding the
|
||||||
`jobs` container command with the per-queue `start:*` scripts (e.g. `start:media`,
|
`jobs` container command with the per-queue `start:*` scripts (e.g. `start:media`,
|
||||||
`start:media-transcribe`) — see `apps/jobs/package.json`.
|
`start:media-transcribe`). See `apps/jobs/package.json`.
|
||||||
|
|
||||||
## HLS Video Playback
|
## HLS Video Playback
|
||||||
|
|
||||||
@@ -259,8 +281,8 @@ docker compose --env-file .env -p classroomio -f docker-compose.yaml up -d --bui
|
|||||||
# Both (e.g. shared packages/* changed)
|
# Both (e.g. shared packages/* changed)
|
||||||
docker compose --env-file .env -p classroomio -f docker-compose.yaml up -d --build api dashboard
|
docker compose --env-file .env -p classroomio -f docker-compose.yaml up -d --build api dashboard
|
||||||
|
|
||||||
# Full stack
|
# Full stack (backs up first)
|
||||||
./run-docker-full-stack.sh
|
./classroomio.sh upgrade --build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
@@ -277,7 +299,7 @@ docker container prune -f # free stopped containers
|
|||||||
|
|
||||||
> **Warning:** `docker volume prune` removes volumes not used by any container. If run after `docker compose ... down`, it deletes `postgres-data`, `redis-data`, and `minio-data`. Use only the prunes above to free space safely.
|
> **Warning:** `docker volume prune` removes volumes not used by any container. If run after `docker compose ... down`, it deletes `postgres-data`, `redis-data`, and `minio-data`. Use only the prunes above to free space safely.
|
||||||
|
|
||||||
Then restart: `./run-docker-full-stack.sh`
|
Then restart: `./classroomio.sh start`
|
||||||
|
|
||||||
### "The database system is in recovery mode" (57P03)
|
### "The database system is in recovery mode" (57P03)
|
||||||
|
|
||||||
@@ -293,7 +315,7 @@ docker compose --env-file .env -p classroomio -f docker-compose.yaml restart min
|
|||||||
|
|
||||||
### SMTP Errors in Logs
|
### SMTP Errors in Logs
|
||||||
|
|
||||||
`ECONNREFUSED 127.0.0.1:465` means SMTP is not configured. This does not block startup — configure SMTP or Zoho when you need email.
|
`ECONNREFUSED 127.0.0.1:465` means SMTP is not configured. This does not block startup; configure SMTP or Zoho when you need email.
|
||||||
|
|
||||||
### Port Conflicts
|
### Port Conflicts
|
||||||
|
|
||||||
@@ -304,5 +326,5 @@ lsof -nP -iTCP:3082 -sTCP:LISTEN
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Self-hosting docs](https://classroomio.com/docs/self-hosted/docker) — published guide with Docker, Railway, and Coolify instructions
|
- [Self-hosting docs](https://classroomio.com/docs/self-hosted/docker): published guide with Docker, Railway, and Coolify instructions
|
||||||
- [`docker/docs/PUBLISHING_IMAGES.md`](PUBLISHING_IMAGES.md) — publishing Docker images to Docker Hub
|
- [`docker/docs/PUBLISHING_IMAGES.md`](PUBLISHING_IMAGES.md): publishing Docker images to Docker Hub
|
||||||
|
|||||||
Generated
+200
-139
@@ -635,8 +635,8 @@ importers:
|
|||||||
specifier: 7.0.9
|
specifier: 7.0.9
|
||||||
version: 7.0.9(@astrojs/[email protected])(@emnapi/[email protected])(@emnapi/[email protected])(@types/[email protected])(@vercel/[email protected](@aws-sdk/[email protected])([email protected]([email protected])([email protected])))([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
|
version: 7.0.9(@astrojs/[email protected])(@emnapi/[email protected])(@emnapi/[email protected])(@types/[email protected])(@vercel/[email protected](@aws-sdk/[email protected])([email protected]([email protected])([email protected])))([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
|
||||||
blume:
|
blume:
|
||||||
specifier: 1.1.2
|
specifier: 1.2.0
|
||||||
version: 1.1.2(864af55737aed5e342af6da019f32ece)
|
version: 1.2.0(864af55737aed5e342af6da019f32ece)
|
||||||
mermaid:
|
mermaid:
|
||||||
specifier: ^11.16.0
|
specifier: ^11.16.0
|
||||||
version: 11.16.0
|
version: 11.16.0
|
||||||
@@ -1561,7 +1561,7 @@ importers:
|
|||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
jsrepo:
|
jsrepo:
|
||||||
specifier: ^3.5.1
|
specifier: ^3.5.1
|
||||||
version: 3.5.1([email protected])([email protected]([email protected]))
|
version: 3.5.1(@emnapi/[email protected])(@emnapi/[email protected])([email protected])([email protected]([email protected]))
|
||||||
layerchart:
|
layerchart:
|
||||||
specifier: 2.0.0-next.27
|
specifier: 2.0.0-next.27
|
||||||
version: 2.0.0-next.27(patch_hash=51dabc4b4462058655e01199047915e5582cfd0492e97d6ecd0c244dc17e5a3a)([email protected])
|
version: 2.0.0-next.27(patch_hash=51dabc4b4462058655e01199047915e5582cfd0492e97d6ecd0c244dc17e5a3a)([email protected])
|
||||||
@@ -2166,18 +2166,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
|
resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/[email protected]':
|
|
||||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/[email protected]':
|
|
||||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -2723,10 +2715,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/[email protected]':
|
|
||||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -3019,15 +3007,9 @@ packages:
|
|||||||
'@emnapi/[email protected]':
|
'@emnapi/[email protected]':
|
||||||
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
|
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
|
||||||
|
|
||||||
'@emnapi/[email protected]':
|
|
||||||
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
|
|
||||||
|
|
||||||
'@emnapi/[email protected]':
|
'@emnapi/[email protected]':
|
||||||
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
|
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
|
||||||
|
|
||||||
'@emnapi/[email protected]':
|
|
||||||
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
|
|
||||||
|
|
||||||
'@emnapi/[email protected]':
|
'@emnapi/[email protected]':
|
||||||
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
|
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
|
||||||
|
|
||||||
@@ -4900,9 +4882,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@napi-rs/[email protected]':
|
|
||||||
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
|
||||||
|
|
||||||
'@napi-rs/[email protected]':
|
'@napi-rs/[email protected]':
|
||||||
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
|
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -7645,9 +7624,6 @@ packages:
|
|||||||
'@tsconfig/[email protected]':
|
'@tsconfig/[email protected]':
|
||||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
|
||||||
'@tybys/[email protected]':
|
|
||||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
|
||||||
|
|
||||||
'@tybys/[email protected]':
|
'@tybys/[email protected]':
|
||||||
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
||||||
|
|
||||||
@@ -8772,8 +8748,8 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
|
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
|
||||||
|
|
||||||
blume@1.1.2:
|
blume@1.2.0:
|
||||||
resolution: {integrity: sha512-AnW/vqsP4Fp2t1ZMW6GWYoWNbsoShuaoLBA1xGebW4W2ZH+vNR9UJcxGdOxZ3idF9OkyJbNjZTkY8J3S7JuIAg==}
|
resolution: {integrity: sha512-VA8TJfSgTp1bRvCPJjufZl+coMRcDSQZvQ5Mx4Qut14TjdO1nB6PY2oOv6zZI11IAQpX/ft9l7i1KzNeXXxNLQ==}
|
||||||
engines: {node: '>=22.12.0'}
|
engines: {node: '>=22.12.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -12565,9 +12541,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
|
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
|
resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
|
||||||
|
|
||||||
@@ -16643,7 +16616,7 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
@@ -16700,7 +16673,7 @@ snapshots:
|
|||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.28.5
|
'@babel/parser': 7.28.5
|
||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.29.7
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
jsesc: 3.1.0
|
jsesc: 3.1.0
|
||||||
@@ -16715,7 +16688,7 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16741,7 +16714,7 @@ snapshots:
|
|||||||
'@babel/helper-optimise-call-expression': 7.27.1
|
'@babel/helper-optimise-call-expression': 7.27.1
|
||||||
'@babel/helper-replace-supers': 7.27.1(@babel/[email protected])
|
'@babel/helper-replace-supers': 7.27.1(@babel/[email protected])
|
||||||
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
|
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
|
||||||
'@babel/traverse': 7.28.5
|
'@babel/traverse': 7.29.0
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -16754,7 +16727,7 @@ snapshots:
|
|||||||
'@babel/helper-optimise-call-expression': 7.27.1
|
'@babel/helper-optimise-call-expression': 7.27.1
|
||||||
'@babel/helper-replace-supers': 7.27.1(@babel/[email protected])
|
'@babel/helper-replace-supers': 7.27.1(@babel/[email protected])
|
||||||
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
|
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
|
||||||
'@babel/traverse': 7.28.5
|
'@babel/traverse': 7.29.0
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -16769,8 +16742,8 @@ snapshots:
|
|||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-compilation-targets': 7.27.2
|
'@babel/helper-compilation-targets': 7.28.6
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
lodash.debounce: 4.0.8
|
lodash.debounce: 4.0.8
|
||||||
resolve: 1.22.11
|
resolve: 1.22.11
|
||||||
@@ -16781,7 +16754,7 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.28.5
|
'@babel/traverse': 7.29.0
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -16789,7 +16762,7 @@ snapshots:
|
|||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.28.5
|
'@babel/traverse': 7.28.5
|
||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -16804,7 +16777,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-module-imports': 7.27.1
|
'@babel/helper-module-imports': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.28.5
|
'@babel/traverse': 7.28.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -16813,7 +16786,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-module-imports': 7.28.6
|
'@babel/helper-module-imports': 7.28.6
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.29.0
|
'@babel/traverse': 7.29.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -16822,7 +16795,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/helper-module-imports': 7.28.6
|
'@babel/helper-module-imports': 7.28.6
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.29.0
|
'@babel/traverse': 7.29.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -16842,7 +16815,7 @@ snapshots:
|
|||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-annotate-as-pure': 7.27.3
|
'@babel/helper-annotate-as-pure': 7.27.3
|
||||||
'@babel/helper-wrap-function': 7.28.3
|
'@babel/helper-wrap-function': 7.28.3
|
||||||
'@babel/traverse': 7.28.5
|
'@babel/traverse': 7.29.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -16851,7 +16824,7 @@ snapshots:
|
|||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-member-expression-to-functions': 7.28.5
|
'@babel/helper-member-expression-to-functions': 7.28.5
|
||||||
'@babel/helper-optimise-call-expression': 7.27.1
|
'@babel/helper-optimise-call-expression': 7.27.1
|
||||||
'@babel/traverse': 7.28.5
|
'@babel/traverse': 7.29.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -16860,31 +16833,27 @@ snapshots:
|
|||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/helper-member-expression-to-functions': 7.28.5
|
'@babel/helper-member-expression-to-functions': 7.28.5
|
||||||
'@babel/helper-optimise-call-expression': 7.27.1
|
'@babel/helper-optimise-call-expression': 7.27.1
|
||||||
'@babel/traverse': 7.28.5
|
'@babel/traverse': 7.29.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.0
|
'@babel/traverse': 7.29.0
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/[email protected]': {}
|
|
||||||
|
|
||||||
'@babel/[email protected]': {}
|
'@babel/[email protected]': {}
|
||||||
|
|
||||||
'@babel/[email protected]': {}
|
|
||||||
|
|
||||||
'@babel/[email protected]': {}
|
'@babel/[email protected]': {}
|
||||||
|
|
||||||
'@babel/[email protected]': {}
|
'@babel/[email protected]': {}
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/template': 7.27.2
|
'@babel/template': 7.28.6
|
||||||
'@babel/traverse': 7.28.5
|
'@babel/traverse': 7.29.0
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -16892,7 +16861,7 @@ snapshots:
|
|||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/template': 7.27.2
|
'@babel/template': 7.27.2
|
||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16901,7 +16870,7 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16949,22 +16918,42 @@ snapshots:
|
|||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16981,15 +16970,30 @@ snapshots:
|
|||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.28.6
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.28.6
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17004,52 +17008,92 @@ snapshots:
|
|||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17238,7 +17282,7 @@ snapshots:
|
|||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-module-transforms': 7.28.3(@babel/[email protected])
|
'@babel/helper-module-transforms': 7.28.3(@babel/[email protected])
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.28.6
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.28.5
|
'@babel/traverse': 7.28.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -17522,7 +17566,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.28.6
|
||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.29.7
|
||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
|
|
||||||
'@babel/[email protected](@babel/[email protected])':
|
'@babel/[email protected](@babel/[email protected])':
|
||||||
@@ -17553,7 +17597,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
'@babel/parser': 7.28.5
|
'@babel/parser': 7.28.5
|
||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17568,7 +17612,7 @@ snapshots:
|
|||||||
'@babel/helper-globals': 7.28.0
|
'@babel/helper-globals': 7.28.0
|
||||||
'@babel/parser': 7.28.5
|
'@babel/parser': 7.28.5
|
||||||
'@babel/template': 7.27.2
|
'@babel/template': 7.27.2
|
||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.29.7
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -17587,13 +17631,8 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.29.7
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected]':
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-string-parser': 7.27.1
|
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17833,22 +17872,11 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@emnapi/[email protected]':
|
|
||||||
dependencies:
|
|
||||||
'@emnapi/wasi-threads': 1.1.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@emnapi/[email protected]':
|
'@emnapi/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@emnapi/[email protected]':
|
|
||||||
dependencies:
|
|
||||||
tslib: 2.8.1
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@emnapi/[email protected]':
|
'@emnapi/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -19096,7 +19124,7 @@ snapshots:
|
|||||||
|
|
||||||
'@jest/[email protected]':
|
'@jest/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.29.0
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
babel-plugin-istanbul: 6.1.1
|
babel-plugin-istanbul: 6.1.1
|
||||||
@@ -19310,13 +19338,6 @@ snapshots:
|
|||||||
'@msgpackr-extract/[email protected]':
|
'@msgpackr-extract/[email protected]':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/[email protected]':
|
|
||||||
dependencies:
|
|
||||||
'@emnapi/core': 1.7.1
|
|
||||||
'@emnapi/runtime': 1.11.1
|
|
||||||
'@tybys/wasm-util': 0.10.1
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@napi-rs/[email protected](@emnapi/[email protected])(@emnapi/[email protected])':
|
'@napi-rs/[email protected](@emnapi/[email protected])(@emnapi/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.11.1
|
'@emnapi/core': 1.11.1
|
||||||
@@ -19641,9 +19662,12 @@ snapshots:
|
|||||||
'@oxc-parser/[email protected]':
|
'@oxc-parser/[email protected]':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxc-parser/[email protected]':
|
'@oxc-parser/[email protected](@emnapi/[email protected])(@emnapi/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@napi-rs/wasm-runtime': 1.1.1
|
'@napi-rs/wasm-runtime': 1.1.6(@emnapi/[email protected])(@emnapi/[email protected])
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxc-parser/[email protected]':
|
'@oxc-parser/[email protected]':
|
||||||
@@ -22605,11 +22629,6 @@ snapshots:
|
|||||||
|
|
||||||
'@tsconfig/[email protected]': {}
|
'@tsconfig/[email protected]': {}
|
||||||
|
|
||||||
'@tybys/[email protected]':
|
|
||||||
dependencies:
|
|
||||||
tslib: 2.8.1
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tybys/[email protected]':
|
'@tybys/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -22619,8 +22638,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.28.5
|
'@babel/parser': 7.29.7
|
||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.29.7
|
||||||
'@types/babel__generator': 7.27.0
|
'@types/babel__generator': 7.27.0
|
||||||
'@types/babel__template': 7.4.4
|
'@types/babel__template': 7.4.4
|
||||||
'@types/babel__traverse': 7.28.0
|
'@types/babel__traverse': 7.28.0
|
||||||
@@ -23866,6 +23885,19 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
[email protected](@babel/[email protected]):
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@jest/transform': 29.7.0
|
||||||
|
'@types/babel__core': 7.20.5
|
||||||
|
babel-plugin-istanbul: 6.1.1
|
||||||
|
babel-preset-jest: 29.6.3(@babel/[email protected])
|
||||||
|
chalk: 4.1.2
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
slash: 3.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.28.6
|
||||||
@@ -23878,7 +23910,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/template': 7.27.2
|
'@babel/template': 7.28.6
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
'@types/babel__traverse': 7.28.0
|
'@types/babel__traverse': 7.28.0
|
||||||
@@ -23930,12 +23962,37 @@ snapshots:
|
|||||||
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/[email protected])
|
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/[email protected])
|
||||||
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/[email protected])
|
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/[email protected])
|
||||||
|
|
||||||
|
[email protected](@babel/[email protected]):
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-bigint': 7.8.3(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-import-attributes': 7.27.1(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/[email protected])
|
||||||
|
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/[email protected])
|
||||||
|
|
||||||
[email protected](@babel/[email protected]):
|
[email protected](@babel/[email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
babel-plugin-jest-hoist: 29.6.3
|
babel-plugin-jest-hoist: 29.6.3
|
||||||
babel-preset-current-node-syntax: 1.2.0(@babel/[email protected])
|
babel-preset-current-node-syntax: 1.2.0(@babel/[email protected])
|
||||||
|
|
||||||
|
[email protected](@babel/[email protected]):
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
babel-plugin-jest-hoist: 29.6.3
|
||||||
|
babel-preset-current-node-syntax: 1.2.0(@babel/[email protected])
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -24076,7 +24133,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
blume@1.1.2(864af55737aed5e342af6da019f32ece):
|
blume@1.2.0(864af55737aed5e342af6da019f32ece):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/check': 0.9.9([email protected])([email protected])
|
'@astrojs/check': 0.9.9([email protected])([email protected])
|
||||||
'@astrojs/markdown-satteri': 0.3.4
|
'@astrojs/markdown-satteri': 0.3.4
|
||||||
@@ -25902,7 +25959,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
eventsource-parser: 3.0.6
|
eventsource-parser: 3.0.8
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -25969,7 +26026,7 @@ snapshots:
|
|||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
parseurl: 1.3.3
|
parseurl: 1.3.3
|
||||||
proxy-addr: 2.0.7
|
proxy-addr: 2.0.7
|
||||||
qs: 6.14.0
|
qs: 6.15.0
|
||||||
range-parser: 1.2.1
|
range-parser: 1.2.1
|
||||||
router: 2.2.0
|
router: 2.2.0
|
||||||
send: 1.2.1
|
send: 1.2.1
|
||||||
@@ -26954,7 +27011,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.29.0
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@istanbuljs/schema': 0.1.3
|
'@istanbuljs/schema': 0.1.3
|
||||||
istanbul-lib-coverage: 3.2.2
|
istanbul-lib-coverage: 3.2.2
|
||||||
@@ -26964,7 +27021,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.29.0
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@istanbuljs/schema': 0.1.3
|
'@istanbuljs/schema': 0.1.3
|
||||||
istanbul-lib-coverage: 3.2.2
|
istanbul-lib-coverage: 3.2.2
|
||||||
@@ -27060,10 +27117,10 @@ snapshots:
|
|||||||
|
|
||||||
[email protected](@types/[email protected])([email protected](@types/[email protected])([email protected])):
|
[email protected](@types/[email protected])([email protected](@types/[email protected])([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.29.0
|
||||||
'@jest/test-sequencer': 29.7.0
|
'@jest/test-sequencer': 29.7.0
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
babel-jest: 29.7.0(@babel/[email protected]8.5)
|
babel-jest: 29.7.0(@babel/[email protected]9.0)
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
ci-info: 3.9.0
|
ci-info: 3.9.0
|
||||||
deepmerge: 4.3.1
|
deepmerge: 4.3.1
|
||||||
@@ -27091,10 +27148,10 @@ snapshots:
|
|||||||
|
|
||||||
[email protected](@types/[email protected])([email protected](@types/[email protected])([email protected])):
|
[email protected](@types/[email protected])([email protected](@types/[email protected])([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.29.0
|
||||||
'@jest/test-sequencer': 29.7.0
|
'@jest/test-sequencer': 29.7.0
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
babel-jest: 29.7.0(@babel/[email protected]8.5)
|
babel-jest: 29.7.0(@babel/[email protected]9.0)
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
ci-info: 3.9.0
|
ci-info: 3.9.0
|
||||||
deepmerge: 4.3.1
|
deepmerge: 4.3.1
|
||||||
@@ -27180,7 +27237,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.29.0
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
'@types/stack-utils': 2.0.3
|
'@types/stack-utils': 2.0.3
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
@@ -27276,15 +27333,15 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.29.0
|
||||||
'@babel/generator': 7.28.5
|
'@babel/generator': 7.29.1
|
||||||
'@babel/plugin-syntax-jsx': 7.28.6(@babel/[email protected]8.5)
|
'@babel/plugin-syntax-jsx': 7.28.6(@babel/[email protected]9.0)
|
||||||
'@babel/plugin-syntax-typescript': 7.28.6(@babel/[email protected]8.5)
|
'@babel/plugin-syntax-typescript': 7.28.6(@babel/[email protected]9.0)
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
'@jest/expect-utils': 29.7.0
|
'@jest/expect-utils': 29.7.0
|
||||||
'@jest/transform': 29.7.0
|
'@jest/transform': 29.7.0
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
babel-preset-current-node-syntax: 1.2.0(@babel/[email protected]8.5)
|
babel-preset-current-node-syntax: 1.2.0(@babel/[email protected]9.0)
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
expect: 29.7.0
|
expect: 29.7.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -27484,14 +27541,17 @@ snapshots:
|
|||||||
dompurify: 2.5.8
|
dompurify: 2.5.8
|
||||||
html2canvas: 1.4.1
|
html2canvas: 1.4.1
|
||||||
|
|
||||||
[email protected]([email protected])([email protected]([email protected])):
|
[email protected](@emnapi/[email protected])(@emnapi/[email protected])([email protected])([email protected]([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 14.0.3
|
commander: 14.0.3
|
||||||
oxc-parser: 0.112.0
|
oxc-parser: 0.112.0(@emnapi/[email protected])(@emnapi/[email protected])
|
||||||
unconfig: 7.5.0
|
unconfig: 7.5.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
svelte: 5.46.0
|
svelte: 5.46.0
|
||||||
vue: 3.5.26([email protected])
|
vue: 3.5.26([email protected])
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -28764,8 +28824,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-function: 5.0.1
|
mimic-function: 5.0.1
|
||||||
|
|
||||||
[email protected]: {}
|
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
@@ -28776,7 +28834,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
oniguruma-parser: 0.12.1
|
oniguruma-parser: 0.12.2
|
||||||
regex: 6.1.0
|
regex: 6.1.0
|
||||||
regex-recursion: 6.0.2
|
regex-recursion: 6.0.2
|
||||||
|
|
||||||
@@ -28838,7 +28896,7 @@ snapshots:
|
|||||||
lodash.isequal: 4.5.0
|
lodash.isequal: 4.5.0
|
||||||
vali-date: 1.0.0
|
vali-date: 1.0.0
|
||||||
|
|
||||||
[email protected]:
|
[email protected](@emnapi/[email protected])(@emnapi/[email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.112.0
|
'@oxc-project/types': 0.112.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -28858,10 +28916,13 @@ snapshots:
|
|||||||
'@oxc-parser/binding-linux-x64-gnu': 0.112.0
|
'@oxc-parser/binding-linux-x64-gnu': 0.112.0
|
||||||
'@oxc-parser/binding-linux-x64-musl': 0.112.0
|
'@oxc-parser/binding-linux-x64-musl': 0.112.0
|
||||||
'@oxc-parser/binding-openharmony-arm64': 0.112.0
|
'@oxc-parser/binding-openharmony-arm64': 0.112.0
|
||||||
'@oxc-parser/binding-wasm32-wasi': 0.112.0
|
'@oxc-parser/binding-wasm32-wasi': 0.112.0(@emnapi/[email protected])(@emnapi/[email protected])
|
||||||
'@oxc-parser/binding-win32-arm64-msvc': 0.112.0
|
'@oxc-parser/binding-win32-arm64-msvc': 0.112.0
|
||||||
'@oxc-parser/binding-win32-ia32-msvc': 0.112.0
|
'@oxc-parser/binding-win32-ia32-msvc': 0.112.0
|
||||||
'@oxc-parser/binding-win32-x64-msvc': 0.112.0
|
'@oxc-parser/binding-win32-x64-msvc': 0.112.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -28949,7 +29010,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.29.0
|
||||||
error-ex: 1.3.4
|
error-ex: 1.3.4
|
||||||
json-parse-even-better-errors: 2.3.1
|
json-parse-even-better-errors: 2.3.1
|
||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
@@ -29203,9 +29264,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
|
|
||||||
[email protected]([email protected].6):
|
[email protected]([email protected].19):
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss: 8.5.6
|
postcss: 8.5.19
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -30611,8 +30672,8 @@ snapshots:
|
|||||||
eslint-scope: 8.4.0
|
eslint-scope: 8.4.0
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 4.2.1
|
||||||
espree: 10.4.0
|
espree: 10.4.0
|
||||||
postcss: 8.5.6
|
postcss: 8.5.19
|
||||||
postcss-scss: 4.0.9([email protected].6)
|
postcss-scss: 4.0.9([email protected].19)
|
||||||
postcss-selector-parser: 7.1.1
|
postcss-selector-parser: 7.1.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
svelte: 5.46.0
|
svelte: 5.46.0
|
||||||
|
|||||||
@@ -1,326 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
BUILD_COMPOSE_FILE="${ROOT_DIR}/docker-compose.yaml"
|
|
||||||
IMAGES_COMPOSE_FILE="${ROOT_DIR}/docker-compose.images.yaml"
|
|
||||||
PROJECT_NAME="classroomio"
|
|
||||||
ENV_FILE="${ROOT_DIR}/.env"
|
|
||||||
# Default to PULLING pre-built images (fast). Pass --build to build from source instead.
|
|
||||||
USE_IMAGES=true
|
|
||||||
BUILD_IMAGES=false
|
|
||||||
COMPOSE_FILE="${IMAGES_COMPOSE_FILE}"
|
|
||||||
COMPOSE_PROFILES="--profile minio"
|
|
||||||
|
|
||||||
print_usage() {
|
|
||||||
cat <<'USAGE'
|
|
||||||
Usage: ./run-docker-full-stack.sh [--build] [--no-build] [--no-minio]
|
|
||||||
|
|
||||||
By default this PULLS the pre-built classroomio/* images from Docker Hub at the tag in
|
|
||||||
CIO_VERSION (.env) — no local build, no full repo needed.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--build Build the images from source (docker-compose.yaml) instead of pulling.
|
|
||||||
--no-build With --build, start containers without rebuilding.
|
|
||||||
--no-minio Exclude MinIO (object storage). By default MinIO is included.
|
|
||||||
--images Deprecated alias — pulling images is now the default.
|
|
||||||
-h, --help Show this help message.
|
|
||||||
USAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--build)
|
|
||||||
USE_IMAGES=false
|
|
||||||
BUILD_IMAGES=true
|
|
||||||
COMPOSE_FILE="${BUILD_COMPOSE_FILE}"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--no-build)
|
|
||||||
BUILD_IMAGES=false
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--no-minio)
|
|
||||||
COMPOSE_PROFILES=""
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--images)
|
|
||||||
# Pulling is the default now; kept for backward compatibility.
|
|
||||||
USE_IMAGES=true
|
|
||||||
BUILD_IMAGES=false
|
|
||||||
COMPOSE_FILE="${IMAGES_COMPOSE_FILE}"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
print_usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
print_usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
get_env_value() {
|
|
||||||
local key="$1"
|
|
||||||
local line
|
|
||||||
line="$(grep -E "^${key}=" "${ENV_FILE}" | tail -n 1 || true)"
|
|
||||||
printf '%s' "${line#*=}"
|
|
||||||
}
|
|
||||||
|
|
||||||
upsert_env_value() {
|
|
||||||
local key="$1"
|
|
||||||
local value="$2"
|
|
||||||
local tmp_file
|
|
||||||
|
|
||||||
tmp_file="$(mktemp)"
|
|
||||||
awk -v key="${key}" -v value="${value}" '
|
|
||||||
BEGIN { updated = 0 }
|
|
||||||
$0 ~ ("^" key "=") {
|
|
||||||
if (updated == 0) {
|
|
||||||
print key "=" value;
|
|
||||||
updated = 1;
|
|
||||||
}
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
END {
|
|
||||||
if (updated == 0) {
|
|
||||||
print key "=" value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "${ENV_FILE}" > "${tmp_file}"
|
|
||||||
|
|
||||||
mv "${tmp_file}" "${ENV_FILE}"
|
|
||||||
}
|
|
||||||
|
|
||||||
is_insecure_token_value() {
|
|
||||||
local value="$1"
|
|
||||||
local normalized
|
|
||||||
|
|
||||||
normalized="$(printf '%s' "${value}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
||||||
|
|
||||||
if [[ "${normalized}" =~ ^\"(.*)\"$ ]]; then
|
|
||||||
normalized="${BASH_REMATCH[1]}"
|
|
||||||
elif [[ "${normalized}" =~ ^\'(.*)\'$ ]]; then
|
|
||||||
normalized="${BASH_REMATCH[1]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "${normalized}" in
|
|
||||||
"" | "replace-with-a-long-random-token" | "local-dev-api-key" | "changeme" | "replace-me")
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
replace-with-* | *local-dev* | *change-this* | *your-*)
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_secure_token() {
|
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
|
||||||
openssl rand -hex 32
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v node >/dev/null 2>&1; then
|
|
||||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
|
||||||
python3 -c "import secrets; print(secrets.token_hex(32))"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
date +%s%N | shasum | awk '{ print $1 }'
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_secure_auth_tokens() {
|
|
||||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
|
||||||
touch "${ENV_FILE}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local private_server_key
|
|
||||||
private_server_key="$(get_env_value PRIVATE_SERVER_KEY)"
|
|
||||||
|
|
||||||
# PRIVATE_SERVER_KEY authenticates the dashboard's server-side calls to the API and must be
|
|
||||||
# identical in both services. Generate a strong value when it's empty or a known placeholder.
|
|
||||||
if is_insecure_token_value "${private_server_key}"; then
|
|
||||||
upsert_env_value PRIVATE_SERVER_KEY "$(generate_secure_token)"
|
|
||||||
echo "Generated secure PRIVATE_SERVER_KEY in .env"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_secure_betterauth_secret() {
|
|
||||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
|
||||||
touch "${ENV_FILE}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local secret
|
|
||||||
secret="$(get_env_value BETTER_AUTH_SECRET)"
|
|
||||||
|
|
||||||
# Only (re)generate when the value is empty or a known placeholder; never clobber
|
|
||||||
# a strong secret the user has set themselves.
|
|
||||||
if is_insecure_token_value "${secret}"; then
|
|
||||||
upsert_env_value BETTER_AUTH_SECRET "$(generate_secure_token)"
|
|
||||||
echo "Generated secure BETTER_AUTH_SECRET in .env"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
is_local_origin() {
|
|
||||||
local value="$1"
|
|
||||||
case "${value}" in
|
|
||||||
"" | *localhost* | *127.0.0.1* | *0.0.0.0*)
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Point browser-facing storage URLs at the public domain when DASHBOARD_ORIGIN is not
|
|
||||||
# localhost. MinIO media is served on :9000 — operators must reverse-proxy /media to it.
|
|
||||||
# We only override values that are still empty/localhost so explicit S3/R2 config is kept.
|
|
||||||
derive_public_storage_urls() {
|
|
||||||
local dashboard_origin public_endpoint media_base
|
|
||||||
dashboard_origin="$(get_env_value DASHBOARD_ORIGIN)"
|
|
||||||
public_endpoint="$(get_env_value OBJECT_STORAGE_PUBLIC_ENDPOINT)"
|
|
||||||
media_base="$(get_env_value OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL)"
|
|
||||||
|
|
||||||
if ! is_local_origin "${dashboard_origin}"; then
|
|
||||||
local origin="${dashboard_origin%/}"
|
|
||||||
# Public media is served from the public-download `media` bucket, so deriving its base
|
|
||||||
# URL from the domain is what fixes broken media links in served pages. We deliberately
|
|
||||||
# do NOT touch OBJECT_STORAGE_PUBLIC_ENDPOINT (the S3 API endpoint) — pointing it at the
|
|
||||||
# dashboard origin would break presigned URLs for the non-public videos/documents buckets.
|
|
||||||
if is_local_origin "${media_base}"; then
|
|
||||||
upsert_env_value OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL "${origin}/media"
|
|
||||||
fi
|
|
||||||
echo "Derived OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL from DASHBOARD_ORIGIN (${origin}/media)."
|
|
||||||
echo " -> Ensure your reverse proxy routes ${origin}/media to MinIO (port 9000)."
|
|
||||||
echo " -> For presigned access to the videos/documents buckets behind a domain, expose"
|
|
||||||
echo " MinIO's S3 API on its own route and set OBJECT_STORAGE_PUBLIC_ENDPOINT explicitly."
|
|
||||||
else
|
|
||||||
# Local demo: ensure localhost defaults are present.
|
|
||||||
if [[ -z "${public_endpoint}" ]]; then
|
|
||||||
upsert_env_value OBJECT_STORAGE_PUBLIC_ENDPOINT "http://localhost:9000"
|
|
||||||
fi
|
|
||||||
if [[ -z "${media_base}" ]]; then
|
|
||||||
upsert_env_value OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL "http://localhost:9000/media"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_minio_env() {
|
|
||||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
|
||||||
touch "${ENV_FILE}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local minio_password
|
|
||||||
minio_password="$(get_env_value MINIO_ROOT_PASSWORD)"
|
|
||||||
|
|
||||||
# First provision: randomize credentials whenever the MinIO password is still empty or the
|
|
||||||
# well-known `minioadmin` placeholder shipped in .env.example. (Previously this gated on
|
|
||||||
# OBJECT_STORAGE_ENDPOINT being empty, but .env.example ships it non-empty — so on a normal
|
|
||||||
# copy-the-example first run the randomization was always skipped and minioadmin/minioadmin
|
|
||||||
# survived.) MinIO uses its root credentials as the S3 access key / secret.
|
|
||||||
if [[ -z "${minio_password}" || "${minio_password}" =~ ^[[:space:]]*$ || "${minio_password}" == "minioadmin" ]]; then
|
|
||||||
local minio_user
|
|
||||||
minio_user="cio-$(generate_secure_token | cut -c1-16)"
|
|
||||||
minio_password="$(generate_secure_token)"
|
|
||||||
|
|
||||||
upsert_env_value MINIO_ROOT_USER "${minio_user}"
|
|
||||||
upsert_env_value MINIO_ROOT_PASSWORD "${minio_password}"
|
|
||||||
upsert_env_value OBJECT_STORAGE_ENDPOINT "http://minio:9000"
|
|
||||||
upsert_env_value OBJECT_STORAGE_ACCESS_KEY_ID "${minio_user}"
|
|
||||||
upsert_env_value OBJECT_STORAGE_SECRET_ACCESS_KEY "${minio_password}"
|
|
||||||
upsert_env_value OBJECT_STORAGE_FORCE_PATH_STYLE "true"
|
|
||||||
echo "Provisioned MinIO with randomized credentials in .env"
|
|
||||||
fi
|
|
||||||
|
|
||||||
derive_public_storage_urls
|
|
||||||
}
|
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
echo "Error: docker is not installed or not on PATH."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_secure_auth_tokens
|
|
||||||
ensure_secure_betterauth_secret
|
|
||||||
|
|
||||||
if [[ "${COMPOSE_PROFILES}" == *"minio"* ]]; then
|
|
||||||
ensure_minio_env
|
|
||||||
else
|
|
||||||
# MinIO excluded: the user must supply external object storage, or uploads/media
|
|
||||||
# silently break. Fail fast instead. The endpoint must be set AND not still point at the
|
|
||||||
# bundled MinIO host (which won't be running with --no-minio).
|
|
||||||
external_endpoint="$(get_env_value OBJECT_STORAGE_ENDPOINT)"
|
|
||||||
if [[ -z "${external_endpoint}" || "${external_endpoint}" == *"minio:9000"* ]]; then
|
|
||||||
echo "Error: --no-minio was passed but no external object storage is configured."
|
|
||||||
echo "OBJECT_STORAGE_ENDPOINT is unset or still points at the bundled MinIO (minio:9000)."
|
|
||||||
echo "Configure an external S3-compatible store via OBJECT_STORAGE_* in .env,"
|
|
||||||
echo "or drop --no-minio to use the bundled MinIO."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
derive_public_storage_urls
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Starting ClassroomIO Docker full stack..."
|
|
||||||
if [[ "${COMPOSE_PROFILES}" != "" ]]; then
|
|
||||||
echo "Including MinIO (object storage, default)..."
|
|
||||||
fi
|
|
||||||
if [[ "${USE_IMAGES}" == "true" ]]; then
|
|
||||||
echo "Using pre-built images from Docker Hub (CIO_VERSION=$(get_env_value CIO_VERSION))..."
|
|
||||||
docker compose --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ${COMPOSE_PROFILES} pull
|
|
||||||
docker compose --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ${COMPOSE_PROFILES} up -d
|
|
||||||
elif [[ "${BUILD_IMAGES}" == "true" ]]; then
|
|
||||||
docker compose --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ${COMPOSE_PROFILES} up --build -d
|
|
||||||
else
|
|
||||||
docker compose --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ${COMPOSE_PROFILES} up -d
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Current service status:"
|
|
||||||
docker compose --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ps
|
|
||||||
|
|
||||||
# `docker compose up -d` returns once containers are *started*, not *healthy*. On a fresh
|
|
||||||
# deployment the API runs DB migrations before it listens, so the endpoints aren't reachable
|
|
||||||
# immediately. Poll with a retry instead of a single curl — and never let a not-yet-ready
|
|
||||||
# check abort the script (set -euo pipefail is active), so first runs end cleanly.
|
|
||||||
check_endpoint() {
|
|
||||||
local name="$1" url="$2" curl_flags="$3" attempts="$4"
|
|
||||||
local i=0
|
|
||||||
while ((i < attempts)); do
|
|
||||||
if curl ${curl_flags} --max-time 5 "${url}" >/dev/null 2>&1; then
|
|
||||||
echo "${name} is reachable on ${url}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
i=$((i + 1))
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
echo "Note: ${name} is not reachable yet at ${url} (waited $((attempts * 5))s)."
|
|
||||||
echo " It may still be starting — the first run migrates the database. Follow logs with:"
|
|
||||||
echo " docker compose --env-file \"${ENV_FILE}\" -p \"${PROJECT_NAME}\" -f \"${COMPOSE_FILE}\" logs -f"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
echo
|
|
||||||
echo "Waiting for services to come up (first run can take a minute while the API migrates)..."
|
|
||||||
check_endpoint "API" "http://localhost:3081/" "-fsS" 24
|
|
||||||
check_endpoint "Dashboard" "http://localhost:3082/" "-fsSI" 12
|
|
||||||
else
|
|
||||||
echo
|
|
||||||
echo "curl not found, skipped endpoint checks."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Done. Full stack is running."
|
|
||||||
Reference in New Issue
Block a user