From c4ca1015947e002d2034fcf423d4876cb21e6c75 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20G=C3=BCnrher?=
Date: Sat, 26 Sep 2026 14:30:42 +0200
Subject: [PATCH] =?UTF-8?q?endeavour:=20.env-Rechte,=20Socket-Pr=C3=BCfung?=
=?UTF-8?q?=20und=20HTTPS-Proxy=20korrigiert?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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)
---
docker/endeavour/deploy.sh | 34 +++++++++
docker/endeavour/env.production.example | 4 +-
docker/endeavour/nginx.conf | 4 +
docs/implement-endeavour.md | 97 +++++++++++++++++++------
4 files changed, 115 insertions(+), 24 deletions(-)
diff --git a/docker/endeavour/deploy.sh b/docker/endeavour/deploy.sh
index 0d67235..55fe0a2 100755
--- a/docker/endeavour/deploy.sh
+++ b/docker/endeavour/deploy.sh
@@ -13,6 +13,13 @@
set -euo pipefail
MAREIKE_BASE="${MAREIKE_BASE:-/opt/mareike}"
+
+# Server-spezifische Einstellungen, z. B. MAREIKE_DB_SOCKET_DIR=/var/lib/mysql
+if [[ -f "$MAREIKE_BASE/shared/deploy.conf" ]]; then
+ # shellcheck source=/dev/null
+ source "$MAREIKE_BASE/shared/deploy.conf"
+fi
+
MAREIKE_USER="${MAREIKE_USER:-mareike-docker}"
SRC="$MAREIKE_BASE/src"
@@ -48,6 +55,28 @@ if [[ ! -f "$MAREIKE_ENV_FILE" ]]; then
exit 1
fi
+# Der Container läuft als $MAREIKE_USER und muss die eingebundene .env lesen können.
+# Ist sie unlesbar, ignoriert Laravel sie still und fällt z. B. auf SQLite zurück.
+chown "root:$MAREIKE_GID" "$MAREIKE_ENV_FILE"
+chmod 0640 "$MAREIKE_ENV_FILE"
+
+# Der Container erreicht die KeyHelp-MariaDB nur über den Socket im eingebundenen Verzeichnis /run/mysqld.
+DB_SOCKET="$( (grep -E '^DB_SOCKET=' "$MAREIKE_ENV_FILE" || true) | tail -n 1 | cut -d= -f2- | tr -d "\"' \r")"
+if [[ -z "$DB_SOCKET" ]]; then
+ echo "DB_SOCKET fehlt in $MAREIKE_ENV_FILE (erwartet: DB_SOCKET=/run/mysqld/mysqld.sock)." >&2
+ exit 1
+fi
+if [[ "$(dirname "$DB_SOCKET")" != "/run/mysqld" ]]; then
+ echo "DB_SOCKET muss in /run/mysqld/ liegen (Mount-Punkt im Container), ist aber: $DB_SOCKET" >&2
+ exit 1
+fi
+if [[ ! -S "$MAREIKE_DB_SOCKET_DIR/$(basename "$DB_SOCKET")" ]]; then
+ echo "Kein MariaDB-Socket unter $MAREIKE_DB_SOCKET_DIR/$(basename "$DB_SOCKET")." >&2
+ echo "Pfad prüfen mit: mysql -NBe 'SELECT @@socket;'" >&2
+ echo "Liegt er woanders, MAREIKE_DB_SOCKET_DIR in $MAREIKE_BASE/shared/deploy.conf setzen." >&2
+ exit 1
+fi
+
echo "==> Quellcode auf '$REF' bringen"
git -C "$SRC" fetch --tags --prune --force origin
if git -C "$SRC" show-ref --verify --quiet "refs/remotes/origin/$REF"; then
@@ -70,6 +99,11 @@ done
echo "==> Images bauen"
compose build --pull
+if ! compose run --rm --no-deps app php -r 'exit(is_readable(".env") ? 0 : 1);'; then
+ echo ".env ist im Container nicht lesbar – Besitzer/Rechte von $MAREIKE_ENV_FILE prüfen." >&2
+ exit 1
+fi
+
echo "==> Datenbank migrieren"
compose run --rm --no-deps app php artisan migrate --force
diff --git a/docker/endeavour/env.production.example b/docker/endeavour/env.production.example
index 83a7c2f..cc650df 100644
--- a/docker/endeavour/env.production.example
+++ b/docker/endeavour/env.production.example
@@ -7,8 +7,8 @@ APP_ENV=production
APP_DEBUG=false
# Erzeugen mit: echo "base64:$(openssl rand -base64 32)" – danach NIE mehr ändern (verschlüsselte Daten!)
APP_KEY=
-# Domain des Dach-Tenants "lv"
-APP_URL=https://.meine-anmeldung.digital
+# Die Hauptdomain ist der Dach-Tenant "lv"; Gruppen laufen als .meine-anmeldung.digital
+APP_URL=https://meine-anmeldung.digital
# Kein ASSET_URL: Assets kommen von der jeweiligen Tenant-Domain selbst.
APP_LOCALE=de
diff --git a/docker/endeavour/nginx.conf b/docker/endeavour/nginx.conf
index 83895aa..335064f 100644
--- a/docker/endeavour/nginx.conf
+++ b/docker/endeavour/nginx.conf
@@ -17,6 +17,10 @@ server {
index index.php;
server_tokens off;
+
+ # nginx sieht hinter dem Proxy nur http. Eigene Redirects (z. B. /tinymce → /tinymce/) daher relativ halten,
+ # sonst landet der Browser auf http://.
+ absolute_redirect off;
client_max_body_size 70m;
location / {
diff --git a/docs/implement-endeavour.md b/docs/implement-endeavour.md
index 9ab0d3a..1031972 100644
--- a/docs/implement-endeavour.md
+++ b/docs/implement-endeavour.md
@@ -51,7 +51,8 @@ Browser ──HTTPS──▶ KeyHelp-Webserver (Apache/nginx, Let's Encrypt, ein
/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)
+├── 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
```
@@ -152,8 +153,18 @@ install -m 0750 /opt/mareike/src/docker/endeavour/deploy.sh /opt/mareike/bin/dep
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.
+ 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.
@@ -162,7 +173,7 @@ Weil die Datenbank von KeyHelp verwaltet wird, ist sie automatisch im **KeyHelp-
## 5. `.env` anlegen
```bash
-install -m 0600 /opt/mareike/src/docker/endeavour/env.production.example /opt/mareike/shared/.env
+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
```
@@ -225,7 +236,7 @@ angelegt wird.
App\Models\Tenant::create([
'slug' => 'lv',
'name' => '',
- 'url' => '.meine-anmeldung.digital',
+ 'url' => 'meine-anmeldung.digital',
'email' => '',
'email_finance' => '',
'account_name' => '',
@@ -249,7 +260,7 @@ 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
+1. Auf `https://meine-anmeldung.digital` ganz normal registrieren. Voraussetzung: Abschnitt 7 ist
erledigt und der Mailversand funktioniert.
2. Das Konto zur Administration hochstufen:
@@ -261,20 +272,36 @@ global und werden für jeden aktiven Tenant ausgeführt.
## 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.
+Domain-Schema:
-### 7.1 Einmalig: Proxy-Module aktivieren (Apache)
+- 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
+a2enmod proxy proxy_http headers rewrite
systemctl reload apache2
```
-### 7.2 Domain anlegen
+### 7.2 Domain einrichten
-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.
+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:
@@ -282,17 +309,34 @@ systemctl reload apache2
**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
- 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/
+ 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;
@@ -301,9 +345,6 @@ systemctl reload apache2
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.
@@ -324,7 +365,7 @@ 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
+* * * * * root curl -fsS -o /dev/null -H "Host: meine-anmeldung.digital" http://127.0.0.1:8080/execute-crons
EOF
```
@@ -378,14 +419,26 @@ Enthält ein Release Datenbankänderungen, deshalb vorher ein DB-Backup ziehen.
| 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]` | 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. |
+| `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