# 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 (nur root lesbar) └── 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, in `/opt/mareike/bin/deploy.sh` die Variable `MAREIKE_DB_SOCKET_DIR` auf das Verzeichnis setzen und `DB_SOCKET` in der `.env` anpassen. Weil die Datenbank von KeyHelp verwaltet wird, ist sie automatisch im **KeyHelp-Backup** enthalten. --- ## 5. `.env` anlegen ```bash install -m 0600 /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' => '', 'url' => '.meine-anmeldung.digital', 'email' => '', 'email_finance' => '', 'account_name' => '', 'account_iban' => '', 'account_bic' => '', 'city' => '', 'postcode' => '', '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 -e "UPDATE users SET user_role_main='ROLE_ADMINISTRATOR', user_role_local_group='ROLE_GROUP_LEADER', active=1 WHERE email='';" ``` --- ## 7. Tenant-Domain in KeyHelp einrichten (je Tenant) Diesen Ablauf für den Dach-Tenant `lv` und später für jede weitere Gruppe wiederholen. ### 7.1 Einmalig: Proxy-Module aktivieren (Apache) ```bash a2enmod proxy proxy_http headers systemctl reload apache2 ``` ### 7.2 Domain anlegen 1. KeyHelp → **Domains** → neue Domain für das mareike-Konto, 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 aktivieren und **HTTPS erzwingen** einschalten. 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 ProxyPreserveHost On ProxyRequests Off RequestHeader set X-Forwarded-Proto "https" ProxyPass /.well-known/acme-challenge/ ! ProxyPass / http://127.0.0.1:8080/ ProxyPassReverse / http://127.0.0.1:8080/ ``` **nginx** (falls KeyHelp auf nginx läuft): ```nginx 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; } ``` Die Ausnahme für `/.well-known/acme-challenge/` sorgt dafür, dass KeyHelp die Zertifikate weiter selbst verlängern kann. 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 | |---|---| | 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]` | Socket nicht erreichbar. `SELECT @@socket` prüfen, `MAREIKE_DB_SOCKET_DIR` bzw. `DB_SOCKET` anpassen, nach einem MariaDB-Neustart den Stack mit `deploy.sh compose restart app` neu starten. | | `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). | 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.