Files
florian.wilde 88d7db8c3d
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
fix(abnahme): Healthcheck schweigt, zwei Konventionen praezisiert (#41)
2026-10-04 14:12:28 +00:00

15 KiB

Inbetriebnahme mit Docker Compose

Von einem Host mit Docker bis zur ersten Anmeldung. Rechne mit zwanzig Minuten, davon zehn für den ersten Bau des Abbilds.

Der andere Weg — nativ in einem Proxmox-LXC, ohne Docker — steht in proxmox.md.

1. Voraussetzungen

  • Docker mit dem Compose-Plugin (docker compose version muss antworten, nicht docker-compose)
  • 2 GB RAM und 8 GB Platte reichen; der erste Bau braucht kurzzeitig mehr
  • git, openssl
  • ein Reverse Proxy für TLS — auf demselben Host oder davor, siehe reverse-proxy.md
  • ein DNS-Name, der auf den Proxy zeigt, bevor du anfängst: er wird zu APP_URL

Node und pnpm brauchst du nicht auf dem Host. Beides steckt im Abbild.

2. Quellcode holen

sudo mkdir -p /opt/laufzettel
sudo chown "$USER" /opt/laufzettel
git clone https://git.wilde-server.de/florian.wilde/Laufzettel.git /opt/laufzettel
cd /opt/laufzettel

Das Verzeichnis ist frei wählbar; alle folgenden Befehle laufen darin, weil docker compose die docker-compose.yml und die .env im aktuellen Verzeichnis sucht.

3. Die .env anlegen

cp .env.example .env
openssl rand -base64 48        # das wird APP_SECRET
openssl rand -base64 24        # das wird POSTGRES_PASSWORD

Dann .env öffnen und mindestens diese fünf Werte setzen:

APP_URL=https://laufzettel.firma.de
APP_SECRET=<Ausgabe von openssl rand -base64 48>
POSTGRES_PASSWORD=<Ausgabe von openssl rand -base64 24>
SEED_ADMIN_EMAIL=admin@firma.de
SEED_ADMIN_PASSWORD=<ein Passwort, das du gleich brauchst>

Und für die erste Anmeldung, bis OIDC steht:

AUTH_LOCAL_ENABLED=true

APP_URL ist die öffentliche Adresse, nicht die interne. Hinter einem Reverse Proxy ist es dessen Adresse mit https://, nicht http://127.0.0.1:3000. Aus ihr entstehen die QR-Adressen auf den gedruckten Aufklebern; sie später zu ändern kostet einen Stapel neuer Aufkleber. Der Grund steht ausführlich in reverse-proxy.md.

Fehlen APP_URL, APP_SECRET oder POSTGRES_PASSWORD, startet der Stand gar nicht. Das ist Absicht: docker compose bricht mit APP_SECRET fehlt in der .env ab, statt mit einem Standardwert zu laufen, den jeder kennt.

DATABASE_URL lässt du für den Compose-Stand in Ruhe. Die Adresse zur Datenbank baut sich im Container immer aus POSTGRES_USER, POSTGRES_PASSWORD und POSTGRES_DB zusammen, mit dem Dienstnamen postgres als Host. Der Wert in der .env gilt nur für Werkzeuge, die außerhalb der Container laufen — pnpm dev, pnpm prisma … von einer Entwicklungsmaschine aus. Bis zur Abnahme von #019 las der Compose-Stand ihn mit, und wer dem dokumentierten Weg folgte, bekam einen Stand, der nicht startet: in der .env.example steht dort localhost, und im Container zeigt das ins Leere.

Alle Umgebungsvariablen

Jede steht mit Kommentar in ../../.env.example. Pflicht sind APP_URL, APP_SECRET und POSTGRES_PASSWORD.

Variable Bedeutung
APP_URL Die öffentliche Adresse der Anwendung, mit Schema und ohne Schrägstrich am Ende. Steckt in den QR-Codes der Aufkleber und in der OIDC-Rücksprungadresse. Entscheidet außerdem, ob das Sitzungs-Cookie das Secure-Flag bekommt (bei https:// ja).
APP_SECRET Schlüssel für die Sitzungs-Cookies und für die Verschlüsselung des LDAP-Bind-Passworts. Mindestens 32 Zeichen, erzeugen mit openssl rand -base64 48. Ändert man ihn, sind alle Sitzungen ungültig und gespeicherte Bind-Passwörter unlesbar. Gehört in die Sicherung.
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB Zugangsdaten der Datenbank im Container. Nach dem ersten Start liegen sie im Volume; sie nachträglich in der .env zu ändern ändert die Datenbank nicht und macht den Stand nur unerreichbar.
DATABASE_URL Nur für Werkzeuge außerhalb der Container. Der Compose-Stand baut seine Adresse selbst; siehe oben.
BIND_ADDR, APP_PORT Wohin der Webdienst auf dem Host gebunden wird. Standard 127.0.0.1:3000 — davor gehört ein Reverse Proxy. Nur wenn der Proxy auf einem anderen Rechner läuft, gehört hier die LAN-Adresse des Hosts hin, nie 0.0.0.0 ohne Firewall.
AUTH_LOCAL_ENABLED true schaltet die Anmeldung mit E-Mail und Passwort frei. Für die erste Anmeldung und als Notfallzugang, wenn der Identitätsanbieter ausfällt. Auf false verschwindet das Formular und der Endpunkt lehnt ab.
OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET Der Identitätsanbieter. Leer heißt: kein OIDC. Discovery über <issuer>/.well-known/openid-configuration.
OIDC_NAME Beschriftung des Anmeldeknopfs, etwa „Firmenanmeldung“.
OIDC_SCOPE Angefragte Bereiche. Der Standard openid profile email groups passt zu authentik.md; nur ändern, wenn der Anbieter die Gruppen unter einem anderen Bereich liefert.
OIDC_GROUP_ADMIN, OIDC_GROUP_EDITOR, OIDC_GROUP_TECHNICIAN, OIDC_GROUP_VIEWER Welche Gruppe welche Rolle bekommt. Die Rolle wird bei jeder Anmeldung neu gesetzt; eine hier in der Oberfläche geänderte Rolle überschreibt der nächste Login.
OIDC_CUSTOMER_GROUP_PREFIX Nur im Modus DIENSTLEISTUNG: Gruppen mit diesem Präfix (etwa lz-kunde-MUS) binden einen Benutzer an einen Kunden. Leer heißt: keine Einschränkung.
ZEITPLAN_AKTIV false schaltet den Verzeichnis-Abgleich nach Zeitplan ab. Der Zeitplan läuft im Prozess der Anwendung und sieht jede Minute nach, welche Quelle fällig ist.
APP_VERSION, GIT_SHA Erscheinen unten links in der Oberfläche. Im Compose-Stand sind es Build-Argumente — sie wirken erst beim nächsten --build.
SEED_ADMIN_EMAIL, SEED_ADMIN_PASSWORD Der Administrator, den der Seed anlegt. Das Passwort wird nur als argon2-Hash gespeichert. Fehlt eines, überspringt der Seed ihn — und dann kommst du nicht hinein.
SEED_MODE INTERN (Standard) oder DIENSTLEISTUNG, SERVICE als Alias. Betrifft nur den ersten Seed-Lauf; danach steht der Modus in der Datenbank und wird in der Oberfläche umgeschaltet.
SEED_ORG_NAME, SEED_ORG_KUERZEL Name und Kürzel der eigenen Organisation. Das Kürzel (2 bis 6 Großbuchstaben oder Ziffern) steht auf Etiketten und in der Hostname-Regel.
TEST_DATABASE_URL Nur für die Tests. Im Betrieb ohne Bedeutung.

4. Starten

docker compose up -d --build

Was dabei passiert, in dieser Reihenfolge:

  1. postgres startet und wird über pg_isready als gesund gemeldet
  2. migrate läuft einmal und spielt prisma migrate deploy ein, dann beendet es sich
  3. web startet, sobald migrate sauber durch ist

Ein beendeter migrate-Container ist also kein Fehler, sondern das erwartete Ergebnis. Nachsehen:

docker compose ps
docker compose logs -f web

5. Den Seed einmal von Hand fahren

Der Seed läuft absichtlich nicht automatisch. Ein Einmal-Dienst, der bei jedem Start Daten anlegt, ist ein Dienst, der irgendwann Daten anlegt, die niemand wollte. Du startest ihn selbst, genau einmal:

docker compose run --rm migrate pnpm db:seed

Der Seed legt an:

Inhalt
Grundbestand Betriebsmodus, eigene Organisation, Administrator aus SEED_ADMIN_EMAIL und SEED_ADMIN_PASSWORD
Vorlagen die sieben Abläufe als veröffentlichte Version 1 — PC-Neuinstallation, Onboarding, Offboarding, Server-Inbetriebnahme, Firewall-Rollout, Patchday, Restore-Test
Beispielbestand 3 Abteilungen, 5 Personen, PC-001 und NB-001 mit Tag-Code, 5 vorgedruckte Tags, Schnellstart-Vorgaben je Gerätetyp
Verzeichnis eine deaktivierte Beispielquelle zum Abschauen — Adresse und Bind-Passwort ersetzen, dann einschalten

Er richtet ein, er räumt nicht auf. Ein zweiter Lauf ändert nichts: eine bearbeitete Vorlage, eine geänderte Ziel-OU oder ein umgeschalteter Betriebsmodus überstehen ihn. Du kannst ihn also gefahrlos wiederholen, wenn du nicht sicher bist, ob er durchgelaufen ist.

Mit SEED_MODE=DIENSTLEISTUNG kommen zwei Demo-Kunden dazu. Willst du ohne Beispieldaten starten, setz SEED_ADMIN_EMAIL und SEED_ADMIN_PASSWORD und lösch die Beispiele hinterher in der Oberfläche — die Vorlagen lässt du besser stehen, sie sind der eigentliche Wert.

6. Erste Anmeldung

Erst prüfen, ob die Anwendung überhaupt antwortet — vom Host, denn nach außen bindet sie nicht:

curl -s http://127.0.0.1:3000/api/gesundheit
# {"status":"ok"}

Antwortet das mit {"status":"fehler",…} und Status 503, erreicht die Anwendung die Datenbank nicht. Kommt gar nichts, läuft web nicht — docker compose logs web.

Dann den Reverse Proxy einrichten (reverse-proxy.md), https://laufzettel.firma.de öffnen und mit SEED_ADMIN_EMAIL und SEED_ADMIN_PASSWORD anmelden.

Danach der Reihe nach:

  1. Admin → Einstellungen — Betriebsmodus prüfen, Organisationsname und Kürzel
  2. authentik anbinden und AUTH_LOCAL_ENABLED wieder auf false setzen — bis auf ein einziges lokales Admin-Konto mit Passwort im Passwortmanager, für den Fall, dass der Anbieter ausfällt
  3. Sicherung einrichten, bevor echte Daten drin sind: sicherung.md

Wie es weitergeht, steht in der Anleitung.

Warum die Datenbank zu ist, und was daraus folgt

Der Aufbau ist bewusst so und in tests/betriebsstand.test.ts festgehalten:

  • postgres hat kein ports:-Mapping, nur expose — auf dem Host gibt es keinen Port 5432
  • postgres hängt ausschließlich im Netz intern, das internal: true ist: ein Netz ohne Gateway. Weder Host noch LAN kommen hinein, und die Datenbank kommt auch nicht heraus
  • nur web hängt zusätzlich im Netz extern, weil es den Reverse Proxy, den Identitätsanbieter und das LDAP-Verzeichnis erreichen muss
  • web bindet auf 127.0.0.1:3000

Ein Datenbankport, den es nicht gibt, muss nicht mit einem Passwort verteidigt werden. Das ist die ganze Begründung.

Daraus folgt dreierlei, und das ist der Teil, der im Alltag zählt:

Kein psql vom Host. psql -h localhost -p 5432 findet nichts. Wer in die Daten sehen will, geht durch den Container:

docker compose exec postgres psql -U laufzettel laufzettel

Die Sicherung läuft über den Dienstnamen, nicht über einen Port. Der vollständige Weg steht in sicherung.md:

docker compose exec -T postgres pg_dump -U laufzettel laufzettel | gzip > sicherung-$(date +%F).sql.gz

Externe Werkzeuge kommen nicht daran — kein pgAdmin vom Arbeitsplatz, kein Monitoring-Agent per TCP. Wer das braucht, öffnet den Port nicht, sondern legt einen SSH-Tunnel auf den Host und von dort docker compose exec. Ein ports:-Mapping in die docker-compose.yml zu schreiben lässt den Test rot werden; das ist sein Zweck.

Prüfen lässt sich beides in zwei Zeilen:

nc -z localhost 5432 && echo "FEHLER: offen" || echo "dicht"
docker compose exec web nc -z postgres 5432 && echo "von innen erreichbar"

docker-compose.dev.yml ist nicht für den Betrieb. Diese Datei öffnet PostgreSQL auf 127.0.0.1:5434 und benutzt das Passwort laufzettel. Sie ist für die Entwicklung da und hat auf einem Server nichts zu suchen.

Aktualisieren

cd /opt/laufzettel
git pull
docker compose up -d --build

Die Migrationen laufen dabei von selbst: migrate startet vor web und läuft durch, bevor die Anwendung hochkommt. Der Seed läuft nicht mit — richtig so, er würde nichts ändern, aber er gehört nicht in eine Aktualisierung.

Vor dem Aktualisieren eine Sicherung ziehen. Eine Migration ist nicht rückwärts fahrbar.

Zwischen zwei Versionen lohnt ein Blick in ../../CHANGELOG.md.

Anhalten, neu starten, wegräumen

docker compose restart web      # nur die Anwendung neu starten, etwa nach .env-Änderungen
docker compose down             # alles anhalten, Daten bleiben im Volume
docker compose down -v          # löscht das Volume und damit die Datenbank

down -v löscht die Datenbank. Das Volume heißt laufzettel_datenbank, und mit ihm gehen alle Läufe, alle Vorlagen und alle Tag-Codes. Ein vergebener Tag-Code wird nie wiederverwendet — nach einem down -v ohne Sicherung sind alle gedruckten Aufkleber wertlos, weil ihre Codes in der neuen Datenbank nicht existieren.

Eine Änderung an der .env greift erst nach docker compose up -d; restart allein liest die Datei nicht neu. Änderungen an APP_VERSION oder GIT_SHA sind Build-Argumente und brauchen --build.

Wenn es klemmt

Symptom Ursache und was zu tun ist
APP_SECRET fehlt in der .env beim up Genau das. Der Stand bricht absichtlich ab, statt mit einem Standardwert zu starten. Gilt ebenso für APP_URL und POSTGRES_PASSWORD.
web startet nicht, migrate steht auf exited (1) docker compose logs migrate. Meist erreicht die Migration die Datenbank nicht — oder du hast POSTGRES_PASSWORD nach dem ersten Start geändert. Das Passwort steckt dann noch im Volume.
/api/gesundheit antwortet mit 503 Die Anwendung läuft, die Datenbank antwortet nicht. docker compose ps — ist postgres gesund? Die Antwort nennt absichtlich keinen Grund, weil der Pfad ohne Anmeldung erreichbar ist; der Grund steht in docker compose logs web.
Der Seed bricht mittendrin ab Bis zur Abnahme von #019 fehlte dem Einmal-Dienst APP_SECRET, und das Bind-Passwort der Beispiel-Verzeichnisquelle ließ sich nicht verschlüsseln — abgebrochen wurde erst, nachdem die Vorlagen schon standen. Steht dein docker-compose.yml auf dem aktuellen Stand, ist das behoben; ein erneuter Lauf holt den Rest nach.
Nach dem Login wieder auf der Anmeldeseite Das Cookie ist Secure, die Verbindung aber http. APP_URL auf https://… setzen und HTTPS über den Proxy anbieten.
Die QR-Codes zeigen auf localhost:3000 APP_URL ist nicht oder falsch gesetzt. Die Oberfläche warnt unter Admin und in der Etikettenansicht davor. Aufkleber, die so gedruckt wurden, sind wertlos — bevor du weiterdruckst, APP_URL richtigstellen und docker compose up -d.
Jeder Benutzer bekommt LESER Der Claim groups fehlt im Token. Siehe authentik.md, Abschnitt 8.
Keine Knöpfe tun etwas, die Seite sieht aber richtig aus Du siehst einen Entwicklungsstand, der nicht hydriert. Im Compose-Stand kann das nicht passieren — dort läuft immer der gebaute Stand. Prüf, ob du tatsächlich den Compose-Stand vor dir hast und nicht ein pnpm dev auf demselben Host.

Fehlermeldungen, die in der Oberfläche erscheinen, stehen in ../anleitung/08-fehlerbehebung.md.