CodeCraft läuft als drei Container: codecraft-web liefert den Editor als
statische Dateien aus, codecraft-server das Backend — beide hinter Traefik.
Dazu optional codecraft-luanti, der Spielserver, siehe unten.
Der Editor spricht das Backend über relative Pfade an (/api/v1/…, siehe
apps/web/src/backend.ts). Damit ist es dieselbe Herkunft, und CORS entfällt
vollständig — kein Preflight, keine Access-Control-*-Header, keine zweite
Domain, die man im Frontend konfigurieren müsste.
Traefik entscheidet am Pfad: alles unter /api geht an das Backend, der Rest
an nginx. Die beiden Router tragen deshalb ausgeschriebene Prioritäten
(100 bzw. 1) in docker-compose.yml.
Der andere Weg wäre gewesen, das Backend die statischen Dateien mitliefern zu
lassen — ein Container statt zwei. Das hätte MIME-Typen, Cache-Header und
Bereichsanfragen in apps/server bedeutet, also rund sechzig Zeilen Code, die
nginx besser kann und die niemand testet.
docker network create proxy # falls noch nicht vorhandenTraefik muss die entryPoints web und websecure sowie den certresolver
myresolver kennen — dieselbe Konfiguration wie bei kidslab.de und ZeFix.org.
git clone https://github.com/KidsLabDe/CodeCraft.git
cd CodeCraft
cp .env.example .env
$EDITOR .env # DOMAIN eintragen, ggf. LUANTI_PORT anpassen
mkdir -p daten welt
sudo chown 10001:10001 daten # uid des Nutzers im Server-Container
sudo chown 30000:30000 welt # uid des Nutzers im Luanti-Container
docker compose build
docker compose up -ddocker compose build führt Typprüfung und Tests mit aus. Ein Abbild, das
nicht typprüft, entsteht gar nicht erst — sonst fiele der Fehler erst zur
Laufzeit auf dem Server auf.
Alles liegt in einer Datei: daten/backend.json. Darin stehen die
registrierten Spielserver samt Token, die Turtles, die Sitzungen der Kinder mit
ihren Zauberwörtern und alle gespeicherten Programme.
Bewusst ein gemapptes Verzeichnis und kein Docker-Volume: die Datei soll sich
lesen, mit cp sichern und im Zweifel von Hand reparieren lassen.
# Sichern
cp daten/backend.json sicherung/backend-$(date +%F).json
# Hineinschauen
python3 -m json.tool daten/backend.json | lessDas Verzeichnis muss 10001:10001 gehören (der Nutzer codecraft im
Container, siehe Dockerfile). Gehört es root, startet das Backend, kann aber
nichts schreiben — der Zustand wäre nach jedem Neustart weg.
daten/ steht in .gitignore. Die Zauberwörter der Kinder gehören nicht ins
Repository.
Das Zauberwort ist Anmeldung und Geheimnis zugleich (siehe „Grenzen" in
protocol.md). Was es tragfähig macht, ist die Drossel in
apps/server/src/drossel.ts — und die braucht die echte Adresse des
Kindes.
Hinter Traefik sieht das Backend sonst nur die Adresse des Proxys, und alle
Kinder einer Schule teilten sich einen Zähler: ein einziges Kind, das sich
fünfmal vertippt, sperrte die ganze Klasse aus. Deshalb steht in
docker-compose.yml:
- CODECRAFT_TRUST_PROXY=1Diese Einstellung gehört nur dorthin, wo wirklich ein Proxy davorsteht.
Ohne Proxy darf jeder X-Forwarded-For frei erfinden und die Sperre damit
vollständig umgehen. Der Backend-Container hat deshalb bewusst kein ports:
— er ist ausschließlich über das proxy-Netz erreichbar.
Der gemeinsame Eimer der Drossel greift unabhängig davon und hält auch bei gefälschtem Header.
git pull
docker compose build
docker compose up -dDie Datendatei bleibt unberührt. Speicher.laden füllt Felder auf, die eine
ältere Fassung noch nicht geschrieben hat — ein Zustand aus einer früheren
Version läuft also weiter.
Der Dienst codecraft-luanti baut aus docker/luanti/Dockerfile ein
eigenständiges Spielserver-Image: das offizielle Basisspiel minetest_game
(gepinnt auf Tag 5.8.0) plus der codecraft-Mod aus mods/luanti/codecraft.
Beides kommt bei jedem Build frisch aus dem Quelltext, nur die Welt liegt
außerhalb, im gemappten welt/ (siehe „Einrichten" oben — muss 30000:30000
gehören, dem Nutzer minetest im offiziellen Luanti-Image).
Luanti spricht UDP, kein HTTP — Traefik kann hier nicht vermitteln, der Port
geht über docker-compose.yml direkt nach außen, Standardwert 36767 (siehe
LUANTI_PORT in .env.example). Zum Backend spricht die Mod intern über den
Dienstnamen server (docker/luanti/minetest.conf), ohne Umweg über Traefik
— das Backend hat ja ohnehin kein ports:.
Das ist der bequeme Weg für einen einzelnen Server wie diesen. Der Spielserver
kann davon unabhängig auch woanders laufen (z. B. in einer Schule): Er
registriert sich beim Backend über POST /api/v1/servers/register und pollt
danach, muss die Domain also nur über HTTPS erreichen können, läuft aber
sonst außerhalb dieses Composes. Konfiguration dafür beschreibt
mods/luanti/codecraft/README.md.
docker compose ps # alle drei Dienste, server healthy
docker compose logs -f server
docker compose logs -f luanti # "Beim Backend angemeldet als ..."
curl -s https://$DOMAIN/ | head -5 # Editor
curl -s -X POST https://$DOMAIN/api/v1/pair \
-H 'Content-Type: application/json' -d '{"code":"AAAAA"}'
# -> 404 "Code unbekannt oder abgelaufen": die API antwortet, Traefik trennt
# die Pfade richtig. Kommt stattdessen HTML, greift die Prioritätsregel nicht.Mit Luanti.app verbinden: Server, $DOMAIN, Port aus LUANTI_PORT (Standard
36767).
Der Healthcheck des Backends fragt genau diesen Endpunkt ab und wertet 404 und 429 als gesund — beides heißt, dass der Server antwortet und die Drossel lebt.