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