Files
STCourses/docs/operations.md
T
Lucas WintherandClaude Opus 5.5 69aaedcb43 build: docker image and compose
Dockerfile on oven/bun:1.3 with ffmpeg, git, unzip and python3-pymupdf, a
production install, non-root user, /data volume and a health check;
docker-compose.yml (project stc) with an optional postgres:17 profile and
read-only course and import mounts; docs/operations.md with configuration,
mounts, security headers and the measured idle memory (101.4 MiB for app plus
Postgres after 5 minutes idle).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:17:58 +01:00

5.3 KiB

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

# 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:<port> 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://<path> 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; <data>/courses and <data>/worktrees are always added
STC_IMPORT_ROOTS <data>/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_<GITHUB/GOOGLE/MICROSOFT/GITLAB/DISCORD>_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 <meta> 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.