Files
mareike/docs/implement-endeavour.md
T
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

20 KiB
Raw Blame History

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.

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):

{
  "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:

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.

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):

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:

    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:

    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

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

/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:

/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

/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.

/opt/mareike/bin/deploy.sh artisan tinker
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:

    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)

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):

    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):

    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:

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

/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:

/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://?

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:

/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.