Files
mareike/docs/implement-endeavour.md
th.guentherandClaude Opus 5.5 c4ca101594 endeavour: .env-Rechte, Socket-Prüfung und HTTPS-Proxy korrigiert
- deploy.sh setzt die .env auf root:mareike-docker 0640 und prüft DB_SOCKET/Socket vor dem Build
- optionale shared/deploy.conf für server-spezifische Einstellungen (z. B. MAREIKE_DB_SOCKET_DIR)
- nginx: absolute_redirect off, damit keine http://-Redirects entstehen
- Anleitung: Proxy per mod_rewrite nur unter HTTPS, keine KeyHelp-Weiterleitung, lv auf der Hauptdomain

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 14:30:42 +02:00

464 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# mareike auf endeavour ausrollen (KeyHelp-Server mit Docker)
Diese Anleitung beschreibt, wie mareike **zusätzlich** zum bestehenden Server auf *endeavour* läuft. Endeavour ist ein
Server, der mit KeyHelp verwaltet wird. Die Parent-Domain ist `meine-anmeldung.digital`.
KeyHelp selbst nutzt kein Docker. mareike läuft deshalb in zwei Containern **neben** KeyHelp. KeyHelp übernimmt
weiter, was es gut kann: Domains, Let's-Encrypt-Zertifikate, MariaDB, Postfächer und Backups.
> **Der bestehende Server bleibt unberührt.** Für endeavour gibt es eigene Dateien unter `docker/endeavour/`.
> `docker-compose.prod`, `docker/prod.Dockerfile`, `docker/run-mareike.sh`, `docker/nginx/default.conf` und
> `.dockerignore` werden nicht verändert und von endeavour nicht benutzt.
---
## 1. Überblick
```
Browser ──HTTPS──▶ KeyHelp-Webserver (Apache/nginx, Let's Encrypt, eine Domain je Tenant)
│ Proxy, Host-Header bleibt erhalten
▼
127.0.0.1:8080 ──▶ Container "web" (nginx, liefert public/ aus)
│ FastCGI
▼
Container "app" (PHP 8.5-FPM, mareike)
│ Unix-Socket /run/mysqld/mysqld.sock
▼
KeyHelp-MariaDB (Host)
```
- **Tenant-Erkennung:** `IdentifyTenant` sucht den Tenant über `tenants.url` = Host der Anfrage. Deshalb muss der
Proxy den Host-Header unverändert weitergeben, und jede Tenant-Domain braucht exakt diesen Eintrag in `tenants.url`.
- **HTTPS:** TLS endet bei KeyHelp. Die nginx im Container meldet PHP trotzdem `HTTPS=on`, damit Laravel `https://`-Links
erzeugt. Eine Code-Änderung ist dafür nicht nötig.
- **Port 8080** ist nur an `127.0.0.1` gebunden und von außen nicht erreichbar.
### Dateien im Repo
| Datei | Zweck |
|---|---|
| `docker/endeavour/Dockerfile` | Multi-Stage-Build. Target `app` enthält PHP-FPM, Code, vendor/ und Assets, Target `web` enthält nginx und public/. |
| `docker/endeavour/Dockerfile.dockerignore` | Eigener Build-Kontext. Unter anderem kommt **keine `.env` ins Image**. |
| `docker/endeavour/compose.yaml` | Stack mit den Services `app` und `web`. |
| `docker/endeavour/nginx.conf` | nginx im Container, inkl. `HTTPS on` und echter Client-IP. |
| `docker/endeavour/php.ini` | Upload-Größen, Speicher, OPcache. |
| `docker/endeavour/env.production.example` | Vorlage für die `.env` auf dem Server. |
| `docker/endeavour/deploy.sh` | Deploy-Skript: pullen, bauen, migrieren, starten. |
### Verzeichnisse auf endeavour
```
/opt/mareike/
├── bin/deploy.sh Kopie von docker/endeavour/deploy.sh
├── src/ Git-Checkout (wird bei jedem Deploy aktualisiert)
├── shared/.env Konfiguration mit Zugangsdaten (root, Gruppe mareike-docker lesbar, 0640)
├── shared/deploy.conf optional: Server-Einstellungen für deploy.sh (z. B. MAREIKE_DB_SOCKET_DIR)
└── storage/ Laravel-storage/ (Logs, Sessions, erzeugte PDFs/ZIPs) – persistent
```
---
## 2. Docker installieren
Als root auf endeavour. Das Beispiel gilt für Debian, bei Ubuntu steht in den Pfaden `ubuntu` statt `debian`.
```bash
apt-get update
apt-get install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
Log-Rotation für alle Container (`/etc/docker/daemon.json`):
```json
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "5" }
}
```
Docker soll erst **nach** MariaDB starten, weil der Container das Socket-Verzeichnis `/run/mysqld` einbindet:
```bash
mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/after-mariadb.conf <<'EOF'
[Unit]
After=mariadb.service
Wants=mariadb.service
EOF
systemctl daemon-reload
systemctl restart docker
docker run --rm hello-world
```
> **KeyHelp-Firewall:** Docker legt eigene iptables-Regeln an. Lädt KeyHelp seine Firewall neu, können diese Regeln
> verloren gehen. Die Container sind dann nicht mehr erreichbar (502 im Browser). Abhilfe ist
> `systemctl restart docker`. Weil mareike nur an `127.0.0.1` lauscht, öffnet Docker nach außen keine Ports.
---
## 3. Systemnutzer, Verzeichnisse, Repository
Die Container laufen mit der UID eines eigenen Host-Nutzers, damit `storage/` sauber beschreibbar ist. Der Name weicht
bewusst von den KeyHelp-Konten ab, weil KeyHelp für seine Konten selbst Systemnutzer anlegt.
```bash
getent passwd 2000 || useradd --system --uid 2000 --user-group --no-create-home --shell /usr/sbin/nologin mareike-docker
# Ist UID 2000 belegt, eine andere freie UID wählen. deploy.sh liest sie automatisch aus.
install -d -m 0755 /opt/mareike /opt/mareike/bin
install -d -m 0700 /opt/mareike/shared
install -d -m 0775 -o mareike-docker -g mareike-docker /opt/mareike/storage
```
Deploy-Key für das Repository anlegen (nur Lesezugriff):
```bash
ssh-keygen -t ed25519 -f /root/.ssh/mareike_deploy -N "" -C "endeavour-deploy"
cat /root/.ssh/mareike_deploy.pub # in Gitea beim Repo th.guenther/mareike als Deploy-Key (read-only) eintragen
cat >> /root/.ssh/config <<'EOF'
Host git.zahlenlabyrinth.de
Port 9922
User git
IdentityFile /root/.ssh/mareike_deploy
IdentitiesOnly yes
EOF
git clone ssh://git@git.zahlenlabyrinth.de:9922/th.guenther/mareike.git /opt/mareike/src
install -m 0750 /opt/mareike/src/docker/endeavour/deploy.sh /opt/mareike/bin/deploy.sh
```
> Ändert sich `docker/endeavour/deploy.sh` im Repo, danach den `install`-Befehl erneut ausführen.
---
## 4. Datenbank in KeyHelp anlegen
1. Im KeyHelp-Panel ein Konto für mareike anlegen oder ein vorhandenes wählen, z. B. `anmeldung`. Unter diesem Konto
liegen später auch die Domains.
2. Unter **Datenbanken** eine neue MariaDB-Datenbank mit Benutzer anlegen. Der Benutzer ist `@localhost`. Den
Remote-Zugriff **nicht** aktivieren, denn der Container verbindet sich über den Unix-Socket und gilt damit als
`localhost`.
3. Den Socket-Pfad prüfen:
```bash
mysql -e "SELECT @@socket;" # erwartet: /run/mysqld/mysqld.sock
```
Liegt der Socket woanders, z. B. unter `/var/lib/mysql/mysql.sock`, das **Verzeichnis** in
`/opt/mareike/shared/deploy.conf` eintragen:
```bash
echo 'MAREIKE_DB_SOCKET_DIR=/var/lib/mysql' > /opt/mareike/shared/deploy.conf
```
In der `.env` bleibt das Verzeichnis `/run/mysqld`, denn so heißt der Mount-Punkt im Container. Nur der Dateiname
wird übernommen, im Beispiel also `DB_SOCKET=/run/mysqld/mysql.sock`.
`deploy.sh` prüft vor jedem Build, ob `DB_SOCKET` gesetzt ist und der Socket auf dem Host existiert. Wenn nicht,
bricht es mit einem Hinweis ab.
Weil die Datenbank von KeyHelp verwaltet wird, ist sie automatisch im **KeyHelp-Backup** enthalten.
---
## 5. `.env` anlegen
```bash
install -m 0640 -o root -g mareike-docker /opt/mareike/src/docker/endeavour/env.production.example /opt/mareike/shared/.env
echo "base64:$(openssl rand -base64 32)" # Ergebnis als APP_KEY eintragen
nano /opt/mareike/shared/.env
```
Die wichtigsten Punkte:
- `APP_KEY` wird einmal erzeugt und dann **nie wieder geändert**, weil Laravel damit verschlüsselte Daten sonst nicht
mehr lesen kann.
- `APP_URL` ist die Domain des Dach-Tenants `lv` (siehe Abschnitt 6).
- `DB_*` sind die Daten aus Schritt 4. `DB_HOST=localhost` und `DB_SOCKET` bleiben wie in der Vorlage.
- `MAIL_*`: Am einfachsten ist ein Postfach in KeyHelp, etwa `noreply@meine-anmeldung.digital`. Als Host den
Mail-Hostnamen des Servers eintragen, mit Port 587.
- `WEBDAV_*`: **eigener Zielordner**, damit sich endeavour und der bestehende Server nicht gegenseitig Dateien
überschreiben.
- `PROVIDER` **muss leer bleiben.** Mit `bdp-lv-sachsen` würde `db:seed` die Sachsen-Tenants samt Personendaten
einspielen.
- `ASSET_URL` nicht setzen, dann kommen die Assets von der jeweiligen Tenant-Domain.
- In der Datei keine `${VAR}`-Verweise verwenden, sondern alle Werte ausschreiben.
---
## 6. Erstinstallation
### 6.1 Erstes Deployment
```bash
/opt/mareike/bin/deploy.sh main # oder einen Release-Tag, z. B. 4.9.1
```
Das Skript holt den Code, baut beide Images, legt die Tabellen per `migrate` an und startet die Container. Der erste
Build dauert ein paar Minuten.
Kurztest:
```bash
/opt/mareike/bin/deploy.sh compose ps
curl -sI http://127.0.0.1:8080/ | head -1 # 404 ist hier richtig: es gibt noch keinen Tenant
```
### 6.2 Stammdaten einspielen
```bash
/opt/mareike/bin/deploy.sh artisan db:seed --force
```
Mit leerem `PROVIDER` läuft nur der `ProductionDataSeeder`. Er legt Rollen, Status, Zahlungsarten, Kostenstellentypen
und die Befreiungsgründe an. Den Befehl **nur einmal** ausführen, ein zweiter Lauf würde Duplikate anlegen.
### 6.3 Dach-Tenant `lv` und Cron-Aufgaben anlegen
Der Slug `lv` ist im Code fest als Dach-Ebene verdrahtet (`LvOnlyMiddleware`, Registrierung, Tenant-Verwaltung).
endeavour braucht deshalb genau einen Tenant mit diesem Slug. Seine `url` ist die Domain, die in Abschnitt 7 in KeyHelp
angelegt wird.
```bash
/opt/mareike/bin/deploy.sh artisan tinker
```
```php
App\Models\Tenant::create([
'slug' => 'lv',
'name' => '<Name des Verbands>',
'url' => 'meine-anmeldung.digital',
'email' => '<kontakt@…>',
'email_finance' => '<finanzen@…>',
'account_name' => '<Kontoinhaber>',
'account_iban' => '<IBAN>',
'account_bic' => '<BIC>',
'city' => '<Ort>',
'postcode' => '<PLZ>',
'is_active_local_group' => true,
'has_active_instance' => true,
]);
use App\Enumerations\CronTaskType;
App\Models\CronTask::create(['name' => 'UploadInvoices', 'execution_type' => CronTaskType::CRON_TASK_TYPE_REALTIME]);
App\Models\CronTask::create(['name' => 'CloseCostUnit', 'execution_type' => CronTaskType::CRON_TASK_TYPE_DAILY, 'schedule_time' => '00:05']);
App\Models\CronTask::create(['name' => 'CloseEvent', 'execution_type' => CronTaskType::CRON_TASK_TYPE_DAILY, 'schedule_time' => '00:10']);
App\Models\CronTask::create(['name' => 'NotifyTeam', 'execution_type' => CronTaskType::CRON_TASK_TYPE_DAILY, 'schedule_time' => '20:00']);
```
Die Cron-Aufgaben entsprechen denen des bestehenden Servers (`BdpLvSachsenDataSeeder::installCronTasks()`). Sie gelten
global und werden für jeden aktiven Tenant ausgeführt.
### 6.4 Erste Administratorin / ersten Administrator
1. Auf `https://meine-anmeldung.digital` ganz normal registrieren. Voraussetzung: Abschnitt 7 ist
erledigt und der Mailversand funktioniert.
2. Das Konto zur Administration hochstufen:
```bash
mysql <datenbankname> -e "UPDATE users SET user_role_main='ROLE_ADMINISTRATOR', user_role_local_group='ROLE_GROUP_LEADER', active=1 WHERE email='<adresse>';"
```
---
## 7. Tenant-Domain in KeyHelp einrichten (je Tenant)
Domain-Schema:
- Dach-Tenant `lv`: die **Hauptdomain** `meine-anmeldung.digital`. Sie existiert in KeyHelp bereits samt
Let's-Encrypt-Zertifikat.
- Gruppen: `<gruppe>.meine-anmeldung.digital`, jede als eigene Domain in KeyHelp mit eigenem Zertifikat.
Den folgenden Ablauf für die Hauptdomain und später für jede Gruppe wiederholen.
> **Wichtig:** Nicht die KeyHelp-Funktion **„Weiterleitung“** benutzen. Sie schickt den *Browser* auf
> `http://127.0.0.1:8080`, und das kann nicht funktionieren. Gebraucht wird ein **Proxy**: KeyHelp holt die Seite
> intern vom Container ab und liefert sie selbst per HTTPS mit seinem Zertifikat aus. Das geschieht ausschließlich über
> die Webserver-Direktiven unten.
### 7.1 Einmalig: Module aktivieren (Apache)
```bash
a2enmod proxy proxy_http headers rewrite
systemctl reload apache2
```
### 7.2 Domain einrichten
1. **Hauptdomain:** In KeyHelp die vorhandene Domain `meine-anmeldung.digital` öffnen. Eine eventuell eingerichtete
*Weiterleitung* entfernen.
**Gruppe:** KeyHelp → **Domains** → neue Domain für das mareike-Konto anlegen, z. B.
`wilde-moehre.meine-anmeldung.digital`. Den DNS-Eintrag (A/AAAA) auf endeavour setzen, falls KeyHelp den DNS nicht
selbst verwaltet.
2. **SSL/TLS:** Let's Encrypt aktiv und **HTTPS erzwingen** eingeschaltet. Bei der Hauptdomain muss
`www.meine-anmeldung.digital` im Zertifikat enthalten sein, sonst gibt es beim www-Redirect eine
Zertifikatswarnung.
3. PHP wird für diese Domain nicht gebraucht.
4. In den Domain-Einstellungen im Feld für **individuelle Webserver-Direktiven** (nur als Admin sichtbar, die
Bezeichnung variiert je nach KeyHelp-Version) Folgendes eintragen:
**Apache** (KeyHelp-Standard):
```apache
RewriteEngine On
# Let's Encrypt von KeyHelp nicht anfassen
RewriteRule ^/?\.well-known/acme-challenge/ - [L]
# www → ohne www
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^/?(.*)$ https://%1/$1 [R=301,L,NE]
# alles ohne TLS auf https umleiten
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*)$ https://%{HTTP_HOST}/$1 [R=301,L,NE]
# unter https an den Container durchreichen
RewriteRule ^/?(.*)$ http://127.0.0.1:8080/$1 [P,L,NE]
ProxyPreserveHost On
ProxyPassReverse / http://127.0.0.1:8080/
RequestHeader set X-Forwarded-Proto "https"
Header always set Strict-Transport-Security "max-age=31536000" "expr=%{HTTPS} == 'on'"
```
Die Regeln funktionieren unabhängig davon, ob KeyHelp die Direktiven in den Port-80- oder den Port-443-vHost
schreibt. Ohne TLS wird immer auf `https://` umgeleitet, nur unter HTTPS wird an den Container weitergereicht. Das
`http://127.0.0.1:8080` ist dabei die **interne** Verbindung zum Container, der Browser sieht sie nie.
**nginx** (falls KeyHelp auf nginx läuft):
```nginx
if ($scheme != "https") {
return 301 https://$host$request_uri;
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
client_max_body_size 70m;
}
```
5. Speichern und die Domain aufrufen. Solange der Tenant noch nicht existiert, zeigt mareike **404 „Tenant not
found“**. Das ist ein gutes Zeichen, denn der Proxy funktioniert.
### 7.3 Tenant in mareike anlegen
Im Dach-Tenant `lv` unter der Tenant-Verwaltung den neuen Tenant anlegen. Als URL **exakt** die Domain ohne `https://`
und ohne Schrägstrich eintragen, z. B. `wilde-moehre.meine-anmeldung.digital`.
Für den Tenant `lv` selbst ist das bereits in Schritt 6.3 passiert.
---
## 8. Cron einrichten
mareike hat keinen Laravel-Scheduler. Die Aufgaben laufen über die URL `/execute-crons`, die für **alle** aktiven
Tenants arbeitet. Sie muss einmal pro Minute aufgerufen werden, dabei genügt der Host-Header eines beliebigen aktiven
Tenants:
```bash
cat > /etc/cron.d/mareike <<'EOF'
* * * * * root curl -fsS -o /dev/null -H "Host: meine-anmeldung.digital" http://127.0.0.1:8080/execute-crons
EOF
```
Kontrolle: `/opt/mareike/storage/logs/` enthält die Task-Logs.
---
## 9. Updates ausrollen
```bash
/opt/mareike/bin/deploy.sh 4.9.2 # Release-Tag
/opt/mareike/bin/deploy.sh main # oder Branch
```
Das Skript läuft in dieser Reihenfolge:
1. `git fetch` und Checkout des Standes.
2. `storage/`-Ordner prüfen.
3. Images neu bauen, dabei werden Basis-Images mit Sicherheitsupdates frisch gezogen.
4. `migrate --force` ausführen.
5. Container neu starten.
6. Kompilierte Views leeren.
7. Alte Images aufräumen.
Nützliche Kurzbefehle:
```bash
/opt/mareike/bin/deploy.sh compose ps
/opt/mareike/bin/deploy.sh compose logs -f web
/opt/mareike/bin/deploy.sh artisan migrate:status
/opt/mareike/bin/deploy.sh artisan tinker
```
**Rollback:** Den vorherigen Tag ausrollen (`deploy.sh 4.9.1`). Migrationen werden dabei **nicht** zurückgedreht.
Enthält ein Release Datenbankänderungen, deshalb vorher ein DB-Backup ziehen.
---
## 10. Backup
| Was | Wie |
|---|---|
| Datenbank | Über das KeyHelp-Backup, weil die DB von KeyHelp verwaltet wird. |
| `/opt/mareike/storage` | **Nicht** im KeyHelp-Backup. Zusätzlich sichern, z. B. per borg/restic oder rsync auf das Backup-Ziel. |
| `/opt/mareike/shared/.env` | Ebenfalls zusätzlich sichern, besonders wegen des `APP_KEY`. |
| Code und Images | Nicht nötig, lassen sich jederzeit aus dem Repo neu bauen. |
---
## 11. Fehlersuche
| Symptom | Ursache / Lösung |
|---|---|
| `Database file at path […database.sqlite] does not exist` | Die `.env` wird im Container nicht gelesen, Laravel fällt dann auf SQLite zurück. Rechte prüfen (0640, Gruppe `mareike-docker`) und ob `MAREIKE_ENV_FILE` auf die richtige Datei zeigt. |
| Browser landet auf `http://` | In KeyHelp ist eine *Weiterleitung* statt der Proxy-Direktiven eingerichtet, oder es sind noch die alten `ProxyPass`-Direktiven ohne HTTPS-Umleitung aktiv (Abschnitt 7.2). Woher die http-Umleitung kommt, zeigen die drei `curl`-Befehle darunter. |
| 404 „Tenant not found“ | Der Host passt nicht zu `tenants.url` (Tippfehler, `https://` oder `/` im Eintrag), der Tenant ist nicht `has_active_instance`, oder der Proxy reicht den Host nicht durch (`ProxyPreserveHost On` fehlt). |
| 502/503 vom KeyHelp-Webserver | Container laufen nicht (`deploy.sh compose ps`) oder die Firewall wurde neu geladen, dann hilft `systemctl restart docker`. |
| Seite lädt ohne CSS, Mixed Content | `ASSET_URL` ist gesetzt oder die nginx-Config im Container wurde nicht übernommen (`fastcgi_param HTTPS on`). |
| `SQLSTATE[HY000] [2002] No such file or directory` | Im Container liegt kein Socket unter `DB_SOCKET`. Entweder fehlt `DB_SOCKET` in der `.env`, oder der Socket liegt auf dem Host nicht in `/run/mysqld`. Pfad mit `mysql -NBe "SELECT @@socket;"` prüfen und ggf. `MAREIKE_DB_SOCKET_DIR` in `shared/deploy.conf` setzen (Abschnitt 4). Nach einem MariaDB-Neustart hilft oft `deploy.sh compose restart app`. |
| `Permission denied` in storage/ | Besitzer von `/opt/mareike/storage` muss `mareike-docker` sein, der nächste `deploy.sh`-Lauf korrigiert das. |
| Upload bricht ab | Grenzen prüfen: `MAX_INVOICE_FILE_SIZE` in der `.env`, 64 MB in `php.ini`, 70 MB in `nginx.conf`. |
| Mails kommen nicht an | `MAIL_*` prüfen, Log in `storage/logs/`. Absenderadresse muss zum Postfach passen (SPF/DKIM der Domain in KeyHelp). |
Woher kommt eine Umleitung auf `http://`?
```bash
curl -sI http://meine-anmeldung.digital/ | grep -iE '^(HTTP|Location)' # erwartet: 301 → https://meine-anmeldung.digital/
curl -sI https://meine-anmeldung.digital/ | grep -iE '^(HTTP|Location)' # erwartet: 302 → https://meine-anmeldung.digital/login
curl -sI -H 'Host: meine-anmeldung.digital' http://127.0.0.1:8080/ | grep -i '^Location' # Container direkt: https://…/login
```
Zeigt der dritte Befehl `https://`, der zweite aber `http://`, liegt es an den KeyHelp-Einstellungen, nicht an mareike.
Logs:
```bash
/opt/mareike/bin/deploy.sh compose logs --tail=200 app web
ls -lt /opt/mareike/storage/logs/
```
---
## 12. Abgrenzung zum bestehenden Server
| | bestehender Server | endeavour |
|---|---|---|
| Dockerfile | `docker/prod.Dockerfile` | `docker/endeavour/Dockerfile` |
| Compose | `docker-compose.prod` | `docker/endeavour/compose.yaml` |
| nginx | `docker/nginx/default.conf` | `docker/endeavour/nginx.conf` |
| Build-Kontext | `.dockerignore` | `docker/endeavour/Dockerfile.dockerignore` |
| Datenbank | eigene | KeyHelp-MariaDB auf endeavour |
| Tenants | `*.mareike.sachsen.pfadfinden.de` | `*.meine-anmeldung.digital` |
Beide Server nutzen denselben Code-Stand aus demselben Repository, haben aber **getrennte** Datenbanken, `.env`,
WebDAV-Ordner und Tenants. Änderungen an `docker/endeavour/` wirken nur auf endeavour.