# Operating STCourses One server process (Bun) serves the API and the web app. Course content lives in folders; the database holds user data only (accounts, progress, notes, attempts, settings), so deleting the database loses user data but never a course. ## Run with Docker Compose ```sh # SQLite in the data volume (the default) STC_SECRET="$(openssl rand -base64 48)" docker compose up -d # Postgres instead STC_SECRET="$(openssl rand -base64 48)" \ STC_DATABASE_URL=postgres://stc:stc@postgres:5432/stc \ docker compose --profile postgres up -d ``` The first account that signs up becomes the owner. Open `http://localhost:3000` (or `STC_HOST_PORT`). Mounts (all course mounts are read-only; STCourses never changes your originals): | Host | Container | What | |---|---|---| | volume `stc-data` | `/data` | database (SQLite), imported courses (`/data/courses`), studio repos (`/data/studio/repos`) and their published worktrees (`/data/worktrees`), caches (`/data/cache`), AI drafts for non-studio courses (`/data/drafts`) | | `STC_COURSES_HOST` (default `./courses`) | `/courses` | course repos, one folder with a `stcourse.yaml` per course | | `STC_IMPORT_HOST` (default `./import-inbox`) | `/import` | files the owner may import (Packt zips, video folders, PDFs, code zips) | An overlay in `overlays/` (baked into the image) names its course repo by absolute path in `root:`. Mount that repo read-only at the same path inside the container, for example `/home/me/functional-programming-course:/home/me/functional-programming-course:ro`. If the repo is missing, the course shows as broken with a diagnostic and every other course keeps working. Back up the `stc-data` volume (and the Postgres volume `stc-pg` when you use Postgres). ## Configuration | Variable | Default | Meaning | |---|---|---| | `STC_SECRET` | none (required in production) | encrypts users' own AI keys and signs sessions; use 32+ random characters | | `STC_PUBLIC_URL` | `http://localhost:` | the URL users open; sign-in checks the request origin against it | | `STC_PORT` | `3000` | listen port | | `STC_DATABASE_URL` | `sqlite://./data/stcourses.db` (image: `sqlite:///data/stcourses.db`) | `sqlite://` or `postgres://…` | | `STC_DATA_DIR` | `./data` (image: `/data`) | see the table above | | `STC_COURSE_DIRS` | `./overlays:./courses` (image: `/app/overlays:/courses`) | `:`-separated folders scanned for courses; `/courses` and `/worktrees` are always added | | `STC_IMPORT_ROOTS` | `/import-inbox` (image: `/import`) | `:`-separated folders the importer may read | | `STC_UPLOAD_MAX_MB` | `100` | studio upload cap | | `STC_PYTHON` | `python3` | Python with PyMuPDF, for PDF page images and text (installed in the image) | | `STC_TRANSCRIBE_URL`, `STC_TRANSCRIBE_KEY`, `STC_TRANSCRIBE_MODEL` | unset | Whisper-compatible endpoint for video transcripts; unset disables transcription | | `STC_AI_ALLOWED_BASE_URLS` | unset | `,`-separated origins learners' own AI keys may use besides api.anthropic.com and api.openai.com | | `STC_AUTH__ID`, `_SECRET` | unset | social sign-in | | `STC_AUTH_OIDC_ISSUER`, `_ID`, `_SECRET`, `_NAME`, `_SCOPES`, `_DISCOVERY_URL`, `STC_AUTH_GROUPS_CLAIM` | unset | generic OIDC sign-in; group claims drive restricted visibility | | `STC_TRUSTED_ORIGINS` | unset | extra `,`-separated origins allowed to call the sign-in API | Without Docker: `bun install`, then `STC_SECRET=… NODE_ENV=production bun apps/server/src/main.ts`. The importer needs `ffmpeg`, `ffprobe`, `git`, `unzip` and Python with PyMuPDF on the host. ## Security headers - The web app carries its Content-Security-Policy as a `` tag in `apps/web/index.html` (no inline script). A meta tag cannot set `frame-ancestors`; if you need clickjacking protection, add `Content-Security-Policy: frame-ancestors 'self'` (or `X-Frame-Options: SAMEORIGIN`) in your reverse proxy. - Course media (`/api/media/…`) is sent with `X-Content-Type-Options: nosniff`, and SVG, HTML and JavaScript files with `Content-Security-Policy: sandbox`, so author content cannot run script on the app's origin. ## Memory budget The deployment must stay under 2 GB idle. Measured with this compose file (`--profile postgres`), the image built from this repository, and four courses mounted read-only: the sample course, Pure Snake and Systemic Design (both overlays, repos mounted at their own paths), and the imported Packt course "Mastering Docker Essentials" (68 videos, 9 modules, a 59-slide deck, code files; about 800 MB of media). Before idling, one owner signed up and opened the library, a video lesson with its deck, a Pure Snake lesson and the sample exercise in headless Chrome. Then nothing for 5 minutes, and `docker stats --no-stream`: | Container | Memory (idle, after 5 min) | CPU | |---|---|---| | `stc-app-1` (Bun server, SPA bundle built and cached) | 84.7 MiB | 1.00% | | `stc-postgres-1` (postgres:17) | 16.67 MiB | 3.52% | | **Total** | **101.4 MiB** (about 5% of the 2 GB budget) | | Measured 2026-10-30 on Linux 6.12 (Docker Compose v5.5.1, `oven/bun:1.3` = Bun 1.3.14). With SQLite there is no Postgres container, so the total is the app's figure alone. Video and PDF files are streamed from disk with Range requests; they never sit in memory. The SPA bundle is built once, on the first page request, and cached.