Produktivbetrieb
Im Produktivbetrieb läuft fepli aus demselben Image wie auf Ihrem Rechner. Anders ist, was darum herum liegt: TLS, echte Geheimnisse, Backups und ein Ablauf für Updates. Diese Seite zeigt ein vollständiges Setup auf einem Server mit Docker Compose und was Sie auf anderen Plattformen beachten.
Das Image
fepli baut das Image in seinem Release-Workflow mit Railpack und veröffentlicht es in der GitHub Container Registry als ghcr.io/ferienpass/app:general-latest. Es enthält PHP, den Webserver (FrankenPHP), alle Abhängigkeiten und die kompilierten Assets. Sie bauen es nie selbst.
Der Tag bewegt sich: Jedes Release ersetzt das Image dahinter. Um selbst zu bestimmen, wann ein Update stattfindet, deployen Sie per Digest. Nach einem Pull zeigt ihn docker images --digests ghcr.io/ferienpass/app, und Sie legen ihn so fest:
image: ghcr.io/ferienpass/app:general-latest@sha256:4f2a…
Das Varnish-Image, ghcr.io/ferienpass/varnish:latest, wird zusammen mit ihm veröffentlicht. Aktualisieren Sie beide gleichzeitig.
Das Image ist für linux/amd64 gebaut. Melden Sie sich auf jedem Host, der es lädt, bei der Registry an:
docker login ghcr.io --username <your user name>
Ein Server mit Docker Compose
Das Setup der lokalen Instanz, mit einem TLS-Proxy davor, Varnish zwischen Proxy und fepli, echten Geheimnissen und ohne Mail-Catcher. Legen Sie diese Dateien in einen Ordner auf dem Server. Um ohne Varnish zu laufen, nehmen Sie einen Dienst heraus.
compose.yaml
name: fepli
x-fepli: &fepli
image: ghcr.io/ferienpass/app:general-latest
env_file: fepli.env
restart: unless-stopped
depends_on:
mariadb:
condition: service_healthy
redis:
condition: service_started
volumes:
- files:/app/files
- storage:/app/storage
- images:/app/contao-assets/images
- deferred-images:/app/var/deferred-images
services:
proxy:
image: caddy:2
restart: unless-stopped
ports:
- '80:80'
- '443:443'
- '443:443/udp'
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- varnish
varnish:
image: ghcr.io/ferienpass/varnish:latest
restart: unless-stopped
env_file: varnish.env
environment:
VARNISH_STORAGE: malloc,1g
depends_on:
- web
web:
<<: *fepli
# Der Port öffnet sich, sobald die Datenbank migriert ist; Worker und Cron warten darauf.
healthcheck:
test: ['CMD', 'php', '-r', 'exit(@fsockopen("127.0.0.1", (int) (getenv("PORT") ?: 80)) ? 0 : 1);']
interval: 10s
start_period: 10m
worker:
<<: *fepli
environment:
IS_WORKER: '1'
depends_on:
web:
condition: service_healthy
cron:
<<: *fepli
command: ['while true; do php bin/console contao:cron; sleep 60; done']
depends_on:
web:
condition: service_healthy
mariadb:
image: mariadb:12.3
restart: unless-stopped
env_file: mariadb.env
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --innodb-buffer-pool-size=1G
healthcheck:
test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized']
interval: 10s
retries: 10
volumes:
- db:/var/lib/mysql
redis:
image: redis:8
restart: unless-stopped
command: ['redis-server', '--appendonly', 'yes', '--maxmemory-policy', 'noeviction']
volumes:
- redis:/data
volumes:
caddy-data:
caddy-config:
db:
redis:
files:
storage:
images:
deferred-images:
Caddyfile
{
email it@example.fepli.eu
}
example.fepli.eu, www.example.fepli.eu {
# Nur fepli selbst darf den Cache leeren, von innerhalb des Netzes.
@invalidation method BAN PURGE PURGEKEYS
respond @invalidation 405
reverse_proxy varnish:80
}
mariadb.env
MARIADB_ROOT_PASSWORD=…
MARIADB_DATABASE=fepli
MARIADB_USER=fepli
MARIADB_PASSWORD=…
varnish.env
# openssl rand -hex 32, derselbe Wert wie in fepli.env
VARNISH_PURGE_TOKEN=5b0e…
fepli.env ist das vollständige Beispiel aus der Konfiguration, mit Ihrer Domain, Ihrem Mailserver, dem Passwort aus mariadb.env in DATABASE_URL und dem Token aus varnish.env in VARNISH_PURGE_TOKEN.
Anmerkungen zu den einzelnen Teilen:
- Caddy bezieht und erneuert die Zertifikate von Let's Encrypt selbstständig, sobald die Domains auf den Server zeigen. Es reicht den ursprünglichen Host und das Schema weiter, die fepli braucht, um korrekte Links zu bauen. Jeder andere Proxy funktioniert, wenn er dasselbe tut: Er muss
X-Forwarded-Protosetzen, den HeaderHostbeibehalten und Uploads bis 32 MB annehmen (nginx:client_max_body_size 32m). - Weitergeleitete Header. fepli entnimmt die Adresse des Clients und das Schema den Headern
X-Forwarded-ForundX-Forwarded-Protovon Anfragen, die von den Adressen inTRUSTED_PROXIESkommen. Der Standard deckt die Netze ab, die Docker einem Compose-Projekt gibt, deshalb müssen Sie die Variable in diesem Setup nicht setzen. fepli ignoriert die übrigenX-Forwarded-*-Header und entnimmt den Host ausHost. Der Proxy muss diese beiden Header daher selbst setzen und darf nicht weiterreichen, was ein Client sendet. Caddy tut das. - Varnish leitet jede Anfrage an den Dienst
webweiter, egal für welche Domain. Es entferntX-Forwarded-Host,X-Forwarded-Port,X-Forwarded-PrefixundForwardedaus jeder Anfrage und ignoriertCache-Controlin Anfragen ohne das Purge-Token: Auch ein erzwungenes Neuladen im Browser erhält die zwischengespeicherte Seite.VARNISH_STORAGEist die Größe seines Caches, 256 MB, wenn Sie sie weglassen. Mit einem größeren Cache liefert Varnish mehr Seiten aus, ohne fepli zu fragen. Varnish brauchtVARNISH_HOST=varnish:80infepli.env. - Purges. Varnish akzeptiert
BAN,PURGEundPURGEKEYSnur aus dem privaten Netz und, mitVARNISH_PURGE_TOKENinvarnish.env, nur wenn sie dieses Token tragen. fepli sendet es ausfepli.env, beide Dateien brauchen also denselben Wert: Steht das Token nur bei Varnish, scheitert jeder Purge. Auch Ihr Proxy verbindet sich aus dem privaten Netz, deshalb weist er die Purge-Methoden selbst ab. - MariaDB: Geben Sie dem Buffer-Pool etwa die Hälfte des Arbeitsspeichers, den Sie MariaDB geben.
- Nur der Proxy veröffentlicht Ports. MariaDB, Redis, Varnish und fepli sind nur aus dem Compose-Netz erreichbar.
Für mehrere Mandanten führen Sie im Caddyfile alle ihre Domains und die Plattform-Konsole auf, durch Kommas getrennt, wie bei Elmsbrück.
Ohne Varnish
So laufen Sie ohne Varnish (siehe Mit oder ohne Varnish):
- Löschen Sie den Dienst
varnishauscompose.yamlund lassen Sie den Proxy stattdessen vonwebabhängen.varnish.envbrauchen Sie nicht mehr. - Leiten Sie im Caddyfile die Anfragen direkt an fepli:
reverse_proxy web:80. - Setzen Sie in
fepli.envVARNISH_HOST=(leer).
fepli purgt dann nichts. Um Varnish später zu ergänzen, machen Sie die drei Schritte rückgängig.
Erster Start
docker compose up -d
Der Web-Container legt die Tabellen von fepli an, bevor er antwortet, und Worker und Cron starten, sobald er antwortet. docker compose logs -f web zeigt den Fortschritt.
Legen Sie danach den Mandanten und seinen ersten Admin an, wie unter Ein oder mehrere Mandanten beschrieben. Richten Sie das DNS der Domains auf den Server, bevor Sie sie öffnen: Caddy braucht es, um die Zertifikate zu erhalten.
Updates
fepli kündigt im Changelog an, was ein Release ändert. Sichern Sie zuerst die Datenbank, dann:
Update
docker compose pull web worker cron varnish
docker compose stop web worker cron
docker compose up -d
pulllädt die neuen fepli-Images herunter, während die alten weiterlaufen. Haben Sie einen Digest festgelegt, tragen Sie den neuen zuerst incompose.yamlein. Lassen Sievarnishweg, wenn Sie ohne Varnish laufen. MariaDB, Redis und Caddy aktualisieren Sie nach Ihrem eigenen Zeitplan.stopnimmt fepli offline, damit kein alter Container die Datenbank verwendet, während die neue Version ihre Struktur ändert. Mit Varnish bleiben die Seiten erreichbar, die es hält; alles andere antwortet mit einem Fehler, bis der letzte Schritt abgeschlossen ist.up -dstartet die neue Version. Der Web-Container migriert die Datenbank, bevor er antwortet: Er ergänzt, was die neue Version braucht, und entfernt, was sie nicht mehr verwendet. Mit Varnish leert er danach den Cache für jede Domain jedes Mandanten und für die Plattform-Konsole. Worker und Cron starten, sobald der Web-Container antwortet.
Scheitert die Migration, hält der Web-Container an, und Worker und Cron starten nicht. docker compose logs web zeigt den Grund. Beheben Sie die Ursache, oder kehren Sie zum vorherigen Image zurück und spielen Sie das Backup ein.
Um die Migration selbst auszuführen, setzen Sie MIGRATE_ON_START=0 in fepli.env und führen Sie sie zwischen stop und up -d aus. Behalten Sie die Anführungszeichen bei: Das Image führt seinen Befehl über bash -c aus, der ganze Befehl muss also ein einziges Argument sein.
docker compose run --rm web 'php bin/console app:migrate --with-deletes --no-backup'
Proben Sie ein Update auf Ihrer lokalen Instanz oder einer Staging-Instanz, bevor Sie es im Produktivbetrieb ausführen. Mit festgelegtem Digest erhält der Produktivbetrieb dann genau das Image, das Sie ausprobiert haben.
Backups
Sichern Sie zwei Dinge täglich und bewahren Sie die Kopien außerhalb des Servers auf. Die Kopien enthalten die Daten der Familien:
- Verschlüsseln Sie sie, bevor sie den Server verlassen, mit einem Schlüssel, dessen privater Teil nicht auf dem Server liegt. Die Beispiele verwenden age und den öffentlichen Teil Ihres Backup-Schlüssels (
age1…). - Legen Sie eine Aufbewahrungsfrist fest und löschen Sie ältere Kopien, wenn sie endet. Eine Kopie behält Daten, die fepli selbst löscht, wenn ihre Aufbewahrungsfrist endet.
Was Sie sichern:
-
Die Datenbank. Sie enthält alles außer Dateien. Erstellen Sie den Dump als Benutzer, der nur lesen darf, mit dem Passwort in einer Options-Datei statt in der Kommandozeile. Legen Sie den Benutzer einmal im MariaDB-Client an, der nach dem Root-Passwort aus
mariadb.envfragt:docker compose exec mariadb mariadb -uroot -pCREATE USER 'backup'@'localhost' IDENTIFIED BY '…'; GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES ON fepli.* TO 'backup'@'localhost';Tragen Sie das Passwort neben
compose.yamlinbackup.cnfein, nur für Sie lesbar (chmod 600):backup.cnf
[mariadb-dump] user=backup password=…Erstellen Sie dann den Dump, komprimieren und verschlüsseln Sie ihn. Der Dump liest
backup.cnfaus seiner Eingabe:docker compose exec -T mariadb \ mariadb-dump --defaults-extra-file=/dev/stdin --single-transaction fepli < backup.cnf \ | gzip | age -r age1… > fepli-$(date +%F).sql.gz.age -
Die Volumes.
filesundstorageenthalten, was Personen hochgeladen und exportiert haben: Logos, Bilder von Angeboten, Exportdateien. Mit Objektspeicher liegen Anhänge sowie Logo und Anmeldebild jedes Mandanten stattdessen im Bucket für Dateien: Sichern Sie auch diesen Bucket. Die Belege und die Einwilligungsnachweise im Retention-Bucket, wo Sie einen genannt haben, sind Unterlagen, die Sie vorlegen können müssen: Nehmen Sie ihn ebenfalls auf, und den Uploads-Bucket. Die übrigen Volumes enthalten Vorschaubilder und den Suchindex; sichern Sie auch sie, wenn Sie lückenlos wiederherstellen möchten. Zum Beispiel:docker run --rm -v fepli_files:/data:ro alpine tar czf - -C /data . \ | age -r age1… > files-$(date +%F).tgz.age
Redis enthält Aufträge, die noch nicht gelaufen sind, und die Sessions. Es hält sie auf der Platte, ein Neustart verliert also nichts, aber es braucht kein Backup.
Ein Backup, das Sie nie wiederhergestellt haben, ist eine Hoffnung, kein Backup. Stellen Sie hin und wieder eines in einer Instanz wieder her, die wie der Produktivbetrieb geschützt ist. Verwenden Sie dafür nicht Ihre lokale Instanz: Die Kopie enthält die Daten echter Familien.
Skalierung
Wenn ein Server mit dem obigen Setup ausgelastet ist:
-
Läuft die Instanz ohne Varnish? Ergänzen Sie es zuerst. Es beantwortet die meisten Seitenaufrufe, bevor sie den Web-Container erreichen.
-
Mehr Anfragen gleichzeitig: Erhöhen Sie
FRANKENPHP_MAX_THREADSund geben Sie dem Web-Container den Arbeitsspeicher dafür, bis zu 512 MB pro Thread. -
Mehr Hintergrundaufträge: Fügen Sie Worker-Container hinzu. Jeder braucht einen eigenen Consumer-Namen als letzten Teil seiner Warteschlangenadresse, sonst holen zwei Worker denselben Auftrag ab:
MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages/symfony/worker-2. -
Mehr als ein Web-Container: Das geht, sobald Sessions und Sperren in Redis liegen (
SESSION_DSNundLOCK_DSN). Die Volumes müssen dann ein gemeinsamer Speicher sein, auf den alle schreiben können. Starten die Container gemeinsam, migrieren sie die Datenbank abwechselnd: Der erste erledigt die Arbeit, die anderen finden nichts mehr zu tun. -
Der Cron: immer genau einer.
Andere Plattformen
Halten Sie sich auf Kubernetes, Nomad, Dokploy, Coolify und Ähnlichem an dieselbe Form:
- Drei Workloads aus einem Image mit denselben Variablen: das Web (Standardbefehl, Port 80), der Worker (
IS_WORKER=1) und der Cron (ein Replikat). - Das Image läuft als root. Geben Sie ihm keinen anderen Benutzer (Compose
user:, KubernetesrunAsUseroderrunAsNonRoot): Sein Startskript und der Webserver erwarten root. - Varnish sendet jede Anfrage an den Host
webauf Port 80. Nennen Sie den Web-Dienstweboder geben Sie ihm diesen Alias. Geben Sie bei einemVARNISH_PURGE_TOKENVarnish denselben Wert wie dem Web-, dem Worker- und dem Cron-Container. - Vertrauenswürdige Proxys. Auf Docker Swarm, Dokploy und Kubernetes verbinden sich Ihr Proxy und Varnish meist aus
10.0.0.0/8, außerhalb des Standards vonTRUSTED_PROXIES. Setzen Sie die Variable auf das Netz, aus dem sie sich verbinden. Sonst hält fepli jede Anfrage für unverschlüsseltes HTTP, das von der Adresse des Proxys kommt. - Das Image führt seinen Befehl über
/bin/bash -caus. Um den Befehl zu ersetzen, übergeben Sie die ganze Befehlszeile als ein Argument. Setzen Sie auf Kubernetesargs, nichtcommand:commandersetzt den Entrypoint. - Der Worker beendet sich alle fünf Minuten selbst, um mit frischem Speicher neu zu beginnen, und verlässt sich darauf, dass er neu gestartet wird. Plattformen, die vor jedem Neustart immer länger warten, wie Kubernetes, sollten ihn stattdessen in einer Schleife ausführen:
args: ['while true; do php bin/console messenger:consume async --time-limit=300 --memory-limit=128M; done']. - Führen Sie den Cron als dauerhafte Schleife aus, wie in der Compose-Datei, und nicht als Job pro Minute, der jedes Mal einen neuen Container startet.
- Volumes, die sich Web und Worker teilen, und ebenso alle Web-Replikate.
- Der Web-Container migriert die Datenbank beim Start, bevor er seinen Port öffnet. Geben Sie ihm eine Startup-Probe mit genug Zeit dafür, z. B. eine TCP-Probe auf Port 80, die zehn Minuten erlaubt. Rollen Sie ein Update aus, indem Sie zuerst die alten Container stoppen (auf Kubernetes
strategy: Recreate): Die Migration entfernt Spalten, die die alte Version noch liest. Um stattdessen in einem eigenen Job zu migrieren, setzen SieMIGRATE_ON_START=0und führen Siephp bin/console app:migrate --with-deletes --no-backupaus, bevor die neuen Container starten. - Einmalige Befehle führen Sie im Web-Container aus: mit
kubectl execoder im Terminal der Plattform.
Zustellberichte von Brevo
E-Mails verschickt fepli über MAILER_DSN: SMTP, sendmail oder die API von Brevo. Gehen die E-Mails über Brevo, kann Brevo melden, was aus jeder E-Mail wurde: zugestellt, zurückgestellt, abgewiesen, blockiert, als Spam gemeldet, abgemeldet, geöffnet, geklickt. Registrieren Sie den Webhook für diese Berichte mit dem Brevo-Konto von BREVO_API_KEY:
docker compose exec web php bin/console brevo:engagement
Der Webhook zeigt auf die Domain in APP_BASE_URL, die Brevo über HTTPS erreichen muss. Auch eine Installation mit mehreren Mandanten braucht ihn nur einmal: Der Webhook zeigt dann auf die Plattform-Konsole, mit oder ohne --tenant, und fepli gibt jeden Bericht an den Mandanten weiter, der die E-Mail verschickt hat. Registrieren Sie keinen pro Mandant: Brevo sendet jeden Bericht an jeden Webhook des Kontos. Der Webhook fragt bei Brevo immer Zustellereignisse, Öffnungen und Klicks an. Was fepli davon festhält, entscheidet jeder Mandant mit der App Versand nachverfolgen unter Einstellungen → App-Store:
- App aus (der Standard). fepli verwirft jeden Bericht. Der Postausgang des Admin-Panels zeigt nur, dass eine Nachricht versendet wurde, und der
deliveryStatusder gesendeten Nachrichten der API bleibtsent. Eine Installation, die die Zustellung bisher erfasst hat, hält keine neuen Berichte fest, bis ein Mandant die App einschaltet: Keine Migration übernimmt das. - App an. fepli hält Zustellung, Abweisungen, Spam-Meldungen und Abmeldungen fest.
- App an und die Einstellung Öffnungen und Klicks messen an. fepli hält auch Öffnungen und Klicks der Empfänger:innen fest, die gemessen werden dürfen. Ist die Einstellung Nur mit Einwilligung messen an, sind das nur Empfänger:innen, die in ihrem Konto zugestimmt haben. Ist sie aus, sind es alle, die nicht widersprochen haben.
Um eine Öffnung zu messen, wird auf dem Gerät der Empfänger:in ein Tracking-Pixel geladen (§ 25 TDDDG). Solange die App an ist, nennt fepli Brevo mit jeder E-Mail, ob Öffnungen und Klicks der Empfänger:in gemessen werden dürfen (contactPixelTrackingConsent in POST /v3/smtp/email), und trägt jede Entscheidung am Kontakt ein (_PIXEL_TRACKING_CONSENT). Damit Brevo das beachtet, schalten Sie in Ihrem Brevo-Konto Per-contact pixel tracking consent ein (Settings → Contacts). Brevo lässt das Pixel dann nicht weg, sondern zählt Öffnung und Klick ohne die Person, wo nicht gemessen werden darf.
Die Angabe je E-Mail gibt es nur über Brevos API (MAILER_DSN=brevo+api://…). Über SMTP erreicht sie Brevo nicht, und jede Empfänger:in gilt dort als unbekannt. Dann entscheidet Track contacts whose consent is unknown im Brevo-Konto: Stellen Sie es auf No, wenn Nur mit Einwilligung messen an ist. Mit Yes misst Brevo auch, wer widersprochen hat; fepli verwirft dessen Öffnungen und Klicks trotzdem.
Ein Webhook, der mit einer früheren Version registriert wurde, fragt möglicherweise andere Ereignisse an. Der Befehl meldet das dann: Führen Sie ihn erneut mit --force aus, um ihn zu ersetzen. Der Konfigurationsschlüssel ferienpass.mail_tracking ist veraltet und wird nicht mehr gelesen. php bin/console mail:purge-tracking zählt die bisher erfassten Öffnungen und Klicks und löscht sie mit --force.
Überwachung
- Logs: Jeder Container schreibt in seine Ausgabe.
docker compose logs -f web worker cronzeigt sie. - Uptime:
https://example.fepli.eu/healthantwortet mit200, solange der Web-Container funktioniert. - Cron: Setzen Sie
STATUS_HEARTBEAT_URLauf die Heartbeat-Adresse eines Uptime-Monitors. Der Cron ruft sie jede Stunde auf; bleiben die Aufrufe aus, schlägt der Monitor Alarm. - Fehler: Sie stehen mit allem anderen in den Logs. fepli verschickt keine Fehlerberichte.
- Der Worker: Wenn keine E-Mails mehr ankommen, sehen Sie zuerst in die Logs des Workers.
Sicherheits-Checkliste
- Nur der Proxy ist von außen erreichbar. MariaDB, Redis und Varnish veröffentlichen nie einen Port.
TRUSTED_HOSTSnennt Ihre Domains und sonst nichts.- Der Proxy setzt
X-Forwarded-ForundX-Forwarded-Protoselbst, statt weiterzureichen, was Clients senden, und behältHostbei. TRUSTED_PROXIESdeckt die Netze ab, aus denen sich Ihr Proxy und Varnish verbinden: in diesem Compose-Setup der Standard, anderswo Ihr eigener Wert.- Mit Varnish weist der Proxy
BAN-,PURGE- undPURGEKEYS-Anfragen von außen ab, und Varnish und fepli teilen ein eigenesVARNISH_PURGE_TOKEN. - Nur das Varnish von fepli legt Seiten von fepli im Cache ab. Ein CDN davor schadet nicht, speichert aber ebenfalls nichts von fepli.
APP_SECRETundINTEGRATIONS_ENCRYPTION_KEYsind Ihre eigenen, undfepli.env,mariadb.envundvarnish.envsind nur für Sie lesbar.ADMIN_EMAIList eine Adresse von Ihnen.APP_DEBUGbleibt0.- Sie spielen neue Releases zeitnah ein: Sicherheitskorrekturen erreichen Sie nur über neue Images.