Zwei Wege in den Betrieb, beide so weit gefahren, wie es hier moeglich war. deploy/install.sh richtet Debian 13 in neun Schritten ein: PostgreSQL 17 hoert nur am Unix-Socket, der Dienstnutzer hat kein Login, die Geheimnisse entstehen im Zielsystem und liegen 0600 bei root. Die Umgebungsdatei wird nie ueberschrieben -- auch nicht beim Aktualisieren, sonst enden alle Sitzungen und die verschluesselten Bind-Passwoerter waeren unlesbar. deploy/proxmox/laufzettel.sh legt den Container an und ruft install.sh darin auf. Der pct-Teil ist ungetestet, weil kein PVE-Host zur Verfuegung stand; die Gegenprobe steht als Pruefliste in docs/betrieb/proxmox.md, das Folge-Issue ist Nummer 38. Die Abnahme gegen alle 61 Kriterien hat drei Zusagen ohne Deckung gefunden, alle behoben: der Healthcheck des Webdienstes konnte nie bestehen (busybox loest localhost nach ::1 auf, Next lauscht nur auf IPv4), die CI-Pipeline konnte in keinem Job gruen werden (kein LDAP-Dienst, kein gestarteter Server, kein shellcheck), und der README nannte fuer die Entwicklungsdatenbank den falschen Port. Closes #19
14 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 versionmuss antworten, nichtdocker-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_URList die öffentliche Adresse, nicht die interne. Hinter einem Reverse Proxy ist es dessen Adresse mithttps://, nichthttp://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 inreverse-proxy.md.
Fehlen
APP_URL,APP_SECREToderPOSTGRES_PASSWORD, startet der Stand gar nicht. Das ist Absicht:docker composebricht mitAPP_SECRET fehlt in der .envab, 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:
postgresstartet und wird überpg_isreadyals gesund gemeldetmigrateläuft einmal und spieltprisma migrate deployein, dann beendet es sichwebstartet, sobaldmigratesauber 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:
- Admin → Einstellungen — Betriebsmodus prüfen, Organisationsname und Kürzel
- authentik anbinden und
AUTH_LOCAL_ENABLEDwieder auffalsesetzen — bis auf ein einziges lokales Admin-Konto mit Passwort im Passwortmanager, für den Fall, dass der Anbieter ausfällt - 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:
postgreshat keinports:-Mapping, nurexpose— auf dem Host gibt es keinen Port 5432postgreshängt ausschließlich im Netzintern, dasinternal: trueist: ein Netz ohne Gateway. Weder Host noch LAN kommen hinein, und die Datenbank kommt auch nicht heraus- nur
webhängt zusätzlich im Netzextern, weil es den Reverse Proxy, den Identitätsanbieter und das LDAP-Verzeichnis erreichen muss webbindet auf127.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 -vlöscht die Datenbank. Das Volume heißtlaufzettel_datenbank, und mit ihm gehen alle Läufe, alle Vorlagen und alle Tag-Codes. Ein vergebener Tag-Code wird nie wiederverwendet — nach einemdown -vohne 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? |
| 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.