261 lines
15 KiB
Markdown
261 lines
15 KiB
Markdown
# 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`](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`](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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```dotenv
|
|
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:
|
|
|
|
```dotenv
|
|
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`](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`](../../.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`](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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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`](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](authentik.md)** 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`](sicherung.md)
|
|
|
|
Wie es weitergeht, steht in der [Anleitung](../anleitung/README.md).
|
|
|
|
## 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:
|
|
|
|
```bash
|
|
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`](sicherung.md):
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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`](../../CHANGELOG.md).
|
|
|
|
## Anhalten, neu starten, wegräumen
|
|
|
|
```bash
|
|
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`](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`](../anleitung/08-fehlerbehebung.md).
|