Many teachers never use topics or boards; their material sits in the course's file area, and the tools answered "0 files" for courses holding dozens of worksheets — 21 of 26 courses on the live account. Persönliche, Kurs-, Team- and Geteilte Dateien live in the legacy file store, not in files-storage, and its service is not in the public ingress. The only way in is the legacy client: HTML listings, and GET /files/signedurl for a pre-signed download. core/legacy-files.ts turns that into one path tree — /my, /courses/<course>, /teams/<team>, /shared — resolving names that contain "/", ids anywhere in a path, and wrong or ambiguous names with a message saying what is there. A listing that does not parse throws; it never reads as an empty folder. Some of the legacy client's GET routes write (GET /files/share/ mints a share token), so getFileManagerPage allows only the listing routes, by pattern. Signed URLs are fetched with no credentials and must be https. - MCP: fs_list, fs_tree, fs_find and fs_read; get_course lists course files. - CLI: schulcloud fs ls, tree, find and get, recursive and resumable. - API: /api/fs/list, tree, find and file. - Index: the crawl walks the file manager (INDEX_FILE_MANAGER, on by default), so search covers the text inside those files and sync mirrors them under <course>/Kurs-Dateien. The local instance gains a fixture for all four areas. It needed a loopback, so signed URLs open from the host, and a pre-created bucket, since MinIO does not implement PutBucketCors. 135 tests. Smoke 55/55 live; 57/57 and 55/55 on the local instance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
319 lines
12 KiB
YAML
319 lines
12 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
|
|
|
|
minio-loopback:
|
|
# The legacy file service (the file manager: Persönliche/Kurs-/Team-Dateien)
|
|
# signs upload and download URLs for its storage provider's one endpoint, and
|
|
# the same endpoint serves its own S3 calls. "minio:9000" works inside the
|
|
# compose network and nowhere else, so a browser or the MCP server on this
|
|
# machine was handed URLs it could not open: uploads through the UI failed
|
|
# and downloads could not be tested. seed.sh registers the provider as
|
|
# http://localhost:9900 instead, and this forwards that address to MinIO from
|
|
# inside the api container's own network namespace — so the one URL now
|
|
# works from the api, from the browser and from the host alike.
|
|
image: alpine/socat:1.8.0.1
|
|
network_mode: "service:api"
|
|
command: ["TCP-LISTEN:9900,fork,reuseaddr,bind=127.0.0.1", "TCP:minio:9000"]
|
|
depends_on:
|
|
api: {condition: service_started}
|
|
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:
|