# 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' => '', '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) Domain-Schema: - Dach-Tenant `lv`: die **Hauptdomain** `meine-anmeldung.digital`. Sie existiert in KeyHelp bereits samt Let's-Encrypt-Zertifikat. - Gruppen: `.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.