Every area — courses, rooms, boards, topics, tasks, files, quizzes, teams,
groups, submissions, grades — was checked for data the instance has and the
tools did not show.
Grades and feedback. A teacher's /homework page is a different page from a
student's: grade and comment live in the grading form, one block per
submission, so a teacher account reported every graded submission as having
neither. parseTeacherGrading reads the form, and list_submissions can now
include the written feedback and who handed the work in.
Names. /api/v1 is partly served: courses, users and classes survive in the
deployment's ingress table, and users/{id} is the only route from an id to a
name. Submitters, file creators and course teachers resolve through it, and
degrade to "not visible to this account" where a student may not read them.
Courses, rooms and classes. get_course adds the description, teachers,
member count and weekly timetable from /api/v1/courses. list_classes is new.
get_room reports what the account may do — allowedOperations is an object of
booleans, not the list it was typed as — and applicants and invitation links
where it may manage them.
Board and topic content. Link descriptions, image alt text, drawing and
video-conference titles, the ids behind external tools and H5P content (the
only thing resembling a quiz), and what a deleted element used to be. Topic
Etherpad pads are read like board pads, and htmlToText keeps table columns
apart and drops template indentation.
Files. A scan with no text layer falls back to the preview endpoint, whose
width and outputFormat are undocumented enums, so Claude gets a picture of
the page; list_files reports counts and sizes. Teams stay documented as
unreadable at any API version; their files come later.
What the crawl missed. Tasks attached to topics (18 of 60 on the live
account), each course's own file area, and — behind INDEX_PERSONAL_FILES —
personal files and submissions with their grade comments, so search and
what_changed cover grading. A submission hit points at get_task.
The local instance's preview profile gets an ImageMagick policy that allows
the coders its 7.1.2 build needs; the image's own denies them all.
110 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
301 lines
11 KiB
YAML
301 lines
11 KiB
YAML
# A local Schulcloud, as close to schulcloud-thueringen.de as it can be made
|
|
# without its external identity provider.
|
|
#
|
|
# The application images are the *same* images the real instance runs
|
|
# (quay.io/schulcloudverbund, thr theme, tag 33.40 — see README.md), so the
|
|
# behaviour under test is the deployed behaviour, not a rebuild of main.
|
|
#
|
|
# docker compose up -d core: login, courses, boards, files
|
|
# docker compose --profile tools up -d + h5p, tldraw, collabora
|
|
# docker compose --profile av up -d + virus scanning of uploads
|
|
#
|
|
# Everything here is a throwaway dev instance: the credentials are the upstream
|
|
# development defaults and are published in the upstream repositories. Do not
|
|
# expose any of it beyond localhost.
|
|
|
|
x-sc-version: &sc-version "${SC_VERSION:-33.40}"
|
|
|
|
x-server-image: &server-image
|
|
image: quay.io/schulcloudverbund/schulcloud-server:${SC_VERSION:-33.40}
|
|
env_file: [env/shared.env, env/jwt.env, env/api.env]
|
|
depends_on:
|
|
mongo: {condition: service_healthy}
|
|
valkey: {condition: service_started}
|
|
rabbitmq: {condition: service_healthy}
|
|
restart: unless-stopped
|
|
|
|
services:
|
|
# ---------------------------------------------------------------- infra ---
|
|
mongo:
|
|
image: docker.io/mongo:7
|
|
# Single-node replica set rather than a bare mongod: the server's migration
|
|
# runner opens transactions, which mongo refuses outside a replica set.
|
|
command: ["--replSet", "rs0", "--bind_ip_all"]
|
|
volumes:
|
|
- mongo-data:/data/db
|
|
ports: ["127.0.0.1:27019:27017"]
|
|
healthcheck:
|
|
# Initiates the replica set on first start and reports healthy once the
|
|
# node is actually primary, which is what every other service waits for.
|
|
test: >-
|
|
mongosh --quiet --eval '
|
|
try { rs.status() } catch (e) { rs.initiate({_id:"rs0",members:[{_id:0,host:"mongo:27017"}]}) }
|
|
quit(db.hello().isWritablePrimary ? 0 : 1)'
|
|
interval: 5s
|
|
timeout: 10s
|
|
retries: 30
|
|
start_period: 10s
|
|
restart: unless-stopped
|
|
|
|
valkey:
|
|
# The JWT whitelist. Sessions die when their key expires, exactly as in
|
|
# production — this is the piece that makes local session testing honest.
|
|
image: docker.io/valkey/valkey:8-alpine
|
|
ports: ["127.0.0.1:6381:6379"]
|
|
restart: unless-stopped
|
|
|
|
rabbitmq:
|
|
image: docker.io/rabbitmq:4-management-alpine
|
|
ports: ["127.0.0.1:15673:15672"]
|
|
healthcheck:
|
|
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
|
|
interval: 10s
|
|
timeout: 10s
|
|
retries: 20
|
|
start_period: 20s
|
|
restart: unless-stopped
|
|
|
|
minio:
|
|
# Stands in for the S3 provider the real instance uses. Buckets are created
|
|
# by minio-init below.
|
|
image: quay.io/minio/minio:latest
|
|
command: server /data --console-address ":9001"
|
|
environment:
|
|
MINIO_ROOT_USER: miniouser
|
|
MINIO_ROOT_PASSWORD: miniouser
|
|
volumes:
|
|
- minio-data:/data
|
|
ports:
|
|
- "127.0.0.1:9900:9000" # S3 API
|
|
- "127.0.0.1:9901:9001" # console (miniouser / miniouser)
|
|
healthcheck:
|
|
test: ["CMD", "mc", "ready", "local"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 30
|
|
restart: unless-stopped
|
|
|
|
minio-init:
|
|
image: quay.io/minio/mc:latest
|
|
depends_on:
|
|
minio: {condition: service_healthy}
|
|
entrypoint: ["/bin/sh", "/init.sh"]
|
|
volumes:
|
|
- ./scripts/minio-init.sh:/init.sh:ro
|
|
restart: "no"
|
|
|
|
# ------------------------------------------------------- schulcloud api ---
|
|
api:
|
|
<<: *server-image
|
|
container_name: sc-api
|
|
command: ["dist/apps/server/apps/server.app"]
|
|
ports: ["127.0.0.1:3030:3030"]
|
|
|
|
management:
|
|
# Not part of the running instance — it exposes the seeding and migration
|
|
# endpoints that the real deployment's init job calls, and nothing else.
|
|
<<: *server-image
|
|
command: ["dist/apps/server/apps/management.app"]
|
|
# Port and base path are hardcoded to 3333 and /api in management.app.ts;
|
|
# PORT is not read here.
|
|
ports: ["127.0.0.1:3333:3333"]
|
|
|
|
board-collaboration:
|
|
# The websocket behind column boards. Without it a board renders once and
|
|
# then never updates.
|
|
<<: *server-image
|
|
command: ["dist/apps/server/apps/board-collaboration.app"]
|
|
environment:
|
|
PORT: "4450"
|
|
|
|
admin-api:
|
|
<<: *server-image
|
|
command: ["dist/apps/server/apps/admin-api-server.app"]
|
|
environment:
|
|
PORT: "4030"
|
|
|
|
file-storage:
|
|
image: quay.io/schulcloudverbund/file-storage:${SC_VERSION:-33.40}
|
|
env_file: [env/shared.env, env/jwt.env, env/file-storage.env]
|
|
depends_on:
|
|
mongo: {condition: service_healthy}
|
|
rabbitmq: {condition: service_healthy}
|
|
minio: {condition: service_healthy}
|
|
ports: ["127.0.0.1:4444:4444"]
|
|
restart: unless-stopped
|
|
|
|
file-storage-consumer:
|
|
# The AMQP half of files-storage, and a separate entrypoint from the HTTP
|
|
# one: only `files-storage-consumer.app` registers FilesStorageConsumer, so
|
|
# running the HTTP app alone leaves the `files-storage` exchange with no
|
|
# queue bound to it.
|
|
#
|
|
# The symptom is not a file problem. TaskService.delete awaits
|
|
# deleteFilesOfParent over AMQP before touching the task, so with nothing
|
|
# consuming, deleting a task or a topic hangs until the request timeout and
|
|
# answers 408 REQUEST_TIMEOUT with the entity still there. Copying a course
|
|
# goes the same way.
|
|
image: quay.io/schulcloudverbund/file-storage:${SC_VERSION:-33.40}
|
|
command: ["dist/apps/files-storage-consumer.app.js"]
|
|
env_file: [env/shared.env, env/jwt.env, env/file-storage.env]
|
|
depends_on:
|
|
mongo: {condition: service_healthy}
|
|
rabbitmq: {condition: service_healthy}
|
|
minio: {condition: service_healthy}
|
|
restart: unless-stopped
|
|
|
|
file-preview:
|
|
# Generates thumbnails via ImageMagick, driven off RabbitMQ. Optional: with
|
|
# it absent, files still upload and download, they just have no preview.
|
|
image: quay.io/schulcloudverbund/file-storage:file-preview-${SC_VERSION:-33.40}
|
|
profiles: ["preview"]
|
|
env_file: [env/shared.env, env/jwt.env, env/file-storage.env]
|
|
volumes:
|
|
# The image's own ImageMagick policy denies every coder it needs; see the
|
|
# comment in the file. Without this the profile runs but produces nothing.
|
|
- ./file-preview/policy.xml:/etc/ImageMagick-7/policy.xml:ro
|
|
depends_on:
|
|
rabbitmq: {condition: service_healthy}
|
|
minio: {condition: service_healthy}
|
|
restart: unless-stopped
|
|
|
|
# -------------------------------------------------------------- clients ---
|
|
client:
|
|
# The legacy UI. Still owns "/" and much of the course view.
|
|
image: quay.io/schulcloudverbund/schulcloud-client-thr:${SC_VERSION:-33.40}
|
|
env_file: [env/shared.env, env/jwt.env, env/client.env]
|
|
depends_on: [api]
|
|
ports: ["127.0.0.1:3100:3100"]
|
|
restart: unless-stopped
|
|
|
|
nuxt:
|
|
# The Vue SPA, built for the thr theme. The image is an nginx that
|
|
# templates env vars into its config at start.
|
|
image: quay.io/schulcloudverbund/schulcloud-frontend-thr:${SC_VERSION:-33.40}
|
|
env_file: [env/nuxt.env]
|
|
ports: ["127.0.0.1:4000:4000"]
|
|
restart: unless-stopped
|
|
|
|
etherpad:
|
|
# The collaborative text editor element.
|
|
#
|
|
# Core, not a "tool", however much it looks like one: the legacy client asks
|
|
# the server for an Etherpad session on *every* topic page whose lesson has
|
|
# contents, without checking whether the lesson contains a pad at all
|
|
# (controllers/topics.js builds `etherpadPads` and then never reads it).
|
|
# Unreachable, that call fails, `validUntil` arrives undefined, and
|
|
# `new Date(undefined * 1000)` makes Express reject the session cookie —
|
|
# "option expires is invalid", a 500 on every topic page. The live
|
|
# deployment always runs Etherpad (ETHERPAD_REPLICAS: 1), so keeping it in
|
|
# the default profile is both the working and the faithful choice.
|
|
image: docker.io/etherpad/etherpad:3.3.3
|
|
env_file: [env/etherpad.env]
|
|
volumes:
|
|
- ./etherpad/APIKEY.txt:/opt/etherpad-lite/APIKEY.txt:ro
|
|
depends_on:
|
|
mongo: {condition: service_healthy}
|
|
restart: unless-stopped
|
|
|
|
proxy:
|
|
# The single origin. Everything a browser touches goes through here, so the
|
|
# app sees one host the way it does in production.
|
|
image: docker.io/nginx:1.29-alpine
|
|
volumes:
|
|
- ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
|
ports: ["127.0.0.1:4400:4400"]
|
|
depends_on: [api, client, nuxt]
|
|
restart: unless-stopped
|
|
|
|
# -------------------------------------------------- external tools -------
|
|
h5p-editor:
|
|
image: quay.io/schulcloudverbund/h5p-server:${SC_VERSION:-33.40}
|
|
profiles: ["tools"]
|
|
command: ["dist/apps/h5p-editor.app"]
|
|
env_file: [env/shared.env, env/jwt.env, env/h5p.env]
|
|
environment:
|
|
PORT: "4448"
|
|
depends_on:
|
|
mongo: {condition: service_healthy}
|
|
minio: {condition: service_healthy}
|
|
restart: unless-stopped
|
|
|
|
h5p-staticfiles:
|
|
image: quay.io/schulcloudverbund/h5p-server:static-files-${SC_VERSION:-33.40}
|
|
profiles: ["tools"]
|
|
restart: unless-stopped
|
|
|
|
h5p-libraries:
|
|
# One-shot: installs the H5P content types listed in env/h5p.env into the
|
|
# library bucket. Exits when done; re-run it after changing that list.
|
|
image: quay.io/schulcloudverbund/h5p-server:${SC_VERSION:-33.40}
|
|
profiles: ["tools"]
|
|
command: ["dist/apps/h5p-library-management.app"]
|
|
env_file: [env/shared.env, env/jwt.env, env/h5p.env]
|
|
depends_on:
|
|
mongo: {condition: service_healthy}
|
|
minio-init: {condition: service_completed_successfully}
|
|
restart: "no"
|
|
|
|
tldraw-server:
|
|
# The whiteboard element.
|
|
image: quay.io/schulcloudverbund/tldraw-server:${SC_VERSION:-33.40}
|
|
profiles: ["tools"]
|
|
command: ["dist/apps/tldraw-server.app.js"]
|
|
env_file: [env/shared.env, env/jwt.env, env/tldraw.env]
|
|
depends_on: [valkey, minio]
|
|
restart: unless-stopped
|
|
|
|
tldraw-worker:
|
|
image: quay.io/schulcloudverbund/tldraw-server:${SC_VERSION:-33.40}
|
|
profiles: ["tools"]
|
|
command: ["dist/apps/tldraw-worker.app.js"]
|
|
env_file: [env/shared.env, env/jwt.env, env/tldraw.env]
|
|
depends_on: [valkey, minio]
|
|
restart: unless-stopped
|
|
|
|
collabora:
|
|
# Office document editing. Reached by the browser directly on :9980, the
|
|
# way the real deployment puts it on its own hostname.
|
|
image: docker.io/collabora/code:latest
|
|
profiles: ["tools"]
|
|
environment:
|
|
extra_params: --o:ssl.enable=false --o:ssl.termination=false
|
|
domain: ".*"
|
|
aliasgroup1: "http://localhost:4400"
|
|
ports: ["127.0.0.1:9980:9980"]
|
|
cap_add: ["MKNOD"]
|
|
restart: unless-stopped
|
|
|
|
# ------------------------------------------------------------ antivirus ---
|
|
clamav:
|
|
# ~1.5 GB resident once the signature database loads, hence its own profile.
|
|
image: docker.io/clamav/clamav:1.5.3
|
|
profiles: ["av"]
|
|
volumes:
|
|
- clamav-db:/var/lib/clamav
|
|
restart: unless-stopped
|
|
|
|
clammit:
|
|
image: ghcr.io/dbildungsplattform/clammit:0.9.1
|
|
profiles: ["av"]
|
|
environment:
|
|
CLAMMIT_CLAMD_URL: tcp://clamav:3310
|
|
CLAMMIT_LISTEN: 0.0.0.0:8438
|
|
depends_on: [clamav]
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
mongo-data:
|
|
minio-data:
|
|
clamav-db:
|