PHP
Laravel, Symfony, WordPress und Co. laufen in einem eigenen PHP-Container. Den bekommst du mit einer PHP-Vorlage im Assistenten oder mit PHP-Anwendung für ein leeres Projekt oder ein Repository. Jedes andere Projekt kann ihn später über den Schalter PHP aktivieren im Bereich Laufzeit dazubekommen.
Der Container heißt envoryx-<projekt>-php und betreibt PHP-FPM. Das Image
ghcr.io/envoryx/envoryx-php:<version> ist das offizielle php:<version>-fpm, dazu Composer und alle
Erweiterungen, die Envoryx anbietet - schon fertig einkompiliert. Der Webserver des Projekts spricht
per FastCGI mit ihm. Vom Browser aus sieht das also aus wie ein ganz normales PHP-Setup.


Welche Version
Abschnitt betitelt „Welche Version“Neue Projekte starten mit 8.5. 8.4, 8.3 und 8.2 werden voll unterstützt, und 8.6 gibt es als Vorschau, wenn du schon mal reinschauen willst. Für ältere Anwendungen, die noch nicht umziehen können, stehen 8.1, 8.0 und 7.4 bereit - auch wenn PHP selbst sie nicht mehr pflegt.
Die Vorschau hat noch Lücken: redis, imagick, memcached, amqp, mongodb und Xdebug lassen sich
für 8.6 noch nicht bauen. Der Bereich Laufzeit graut sie für diese Version aus, und Envoryx lehnt sie ab,
statt dich mit einer Erweiterung allein zu lassen, die stillschweigend fehlt.
Die Version stellst du im Bereich Laufzeit um. Envoryx baut seine Images jede Woche aus den offiziellen neu. Ein Neustart des Projekts holt sich ein neues Patch-Release, und wenn ein neues Image etwas kaputt macht, bringt dich Zurücksetzen in der Übersicht zum vorherigen zurück. Mehr dazu unter Projekt-Lebenszyklus.
Webserver und Document Root
Abschnitt betitelt „Webserver und Document Root“Neben PHP hat jedes Projekt einen Webserver-Container. Standard ist Caddy, Nginx gibt es
auch. Beide geben jeden Pfad, hinter dem keine echte Datei liegt, an index.php weiter. Front
Controller laufen deshalb ohne Rewrite-Regeln: die Routen von Laravel und Symfony genauso wie
WordPress-Permalinks à la Beitragsname. Nimm Apache, wenn deine Anwendung eigene Regeln in
.htaccess-Dateien ablegt, etwa ein WordPress-Plugin, das dort Weiterleitungen oder Zugriffsregeln
einträgt. Apache ist der einzige der drei, der sie liest. Du kannst jederzeit zwischen allen dreien
wechseln.
Der Document Root ist der Ordner, den der Webserver ausliefert, relativ zum Projektverzeichnis.
Bei Laravel und Symfony ist das public.
PHP-Dateien in Upload- und Cache-Ordnern laufen nicht, auf keinem der drei Server: Unter
wp-content/uploads von WordPress (auch app/uploads bei Bedrock und Installationen in einem
Unterordner), sites/*/files von Drupal, fileadmin und typo3temp von TYPO3, media und
thumbnail von Shopware und Magento, cpresources und den üblichen Asset-Volumes assets und
images von Craft, dem storage-Link von Laravel und einem schlichten uploads-Ordner bekommt eine
Anfrage nach einer .php-Datei ein 403. Die CMS-Ordner zählen überall, auch bei einer Installation
in einem Unterordner; die allgemeinen Namen (uploads, media, thumbnail, storage, assets,
images) nur direkt unter dem Document Root. Gibst du einem Craft-Volume einen anderen Namen, leg es
unter uploads an. Dorthin legt ein
Upload-Formular seine Dateien, und ein Skript, das auf diesem Weg hineinrutscht, liefe sonst wie dein
eigener Code. Die CMS bringen dafür .htaccess-Regeln mit, die liest aber nur Apache. Bilder und
alles andere in diesen Ordnern werden wie gewohnt ausgeliefert, und Drupals Bildstile entstehen
weiterhin.
HTTPS hinter dem Proxy
Abschnitt betitelt „HTTPS hinter dem Proxy“TLS endet am Proxy von Envoryx, und der Webserver neben PHP bekommt von ihm nur schlichtes http. PHP
würde dann glauben, jeder Request sei über http gekommen, und Frameworks bauen ihre Links und
Weiterleitungen daraus: ein Login-Formular, das an http schickt, die Symfony-Toolbar, die als Mixed
Content blockiert wird, ein Installer, der dich von https wegleitet. Deshalb übersetzen die erzeugten
Configs für Caddy, Nginx und Apache das X-Forwarded-Proto des Proxys in das, was PHP hinter einem
eigenen TLS-Endpunkt sähe: HTTPS=on, REQUEST_SCHEME=https und den Port, den der Browser benutzt hat
(443, wenn in der Adresse keiner steht). Laravel, Symfony, Shopware, TYPO3 und WordPress bauen damit
von selbst https://-Links, und vertrauenswürdige Proxys musst du dafür nicht einrichten. Schlichtes
http und der direkte Port behalten die Werte des
Webservers. SERVER_NAME ist auf allen drei Servern der Host, den der Browser angefragt hat (Nginx
gäbe PHP sonst seinen Platzhalternamen _), und darauf prüft TYPO3 seine Trusted Hosts.
Erweiterungen
Abschnitt betitelt „Erweiterungen“mbstring, curl, pdo_sqlite und exif sind immer an (mbstring in jedem PHP-Image, auch in
8.6, obwohl das offizielle 8.6-Image es nicht mehr einkompiliert; exif, mit dem WordPress, Drupal und
TYPO3 die Ausrichtung von Fotos lesen, fehlt in allen offiziellen Images). Alles andere schaltest du pro Projekt im Bereich
Laufzeit. Weil jede Erweiterung schon im Image steckt, wird dabei nichts neu gebaut.
| Erweiterung | Wofür |
|---|---|
opcache |
Opcode-Cache |
pdo_mysql, mysqli |
MySQL / MariaDB |
pdo_pgsql |
PostgreSQL |
mongodb |
MongoDB-Treiber |
gd, imagick |
Bildverarbeitung, ImageMagick |
intl |
Internationalisierung |
zip |
ZIP-Archive |
bcmath |
Rechnen mit beliebiger Genauigkeit |
redis |
Redis-Client (phpredis) |
memcached |
Memcached-Client |
amqp |
AMQP-Client (RabbitMQ, Symfony Messenger) |
Ein neues Projekt startet mit bcmath, gd, intl, opcache, pdo_mysql und zip. Templates legen
drauf, was ihre Anwendung braucht. Manche Dienste bringen ihren Client gleich mit: PostgreSQL
schaltet pdo_pgsql ein, Redis redis, Memcached memcached und RabbitMQ amqp. Ohne sie brechen
Laravels phpredis-Standard, die Memcached-Cache-Treiber und der AMQP-Transport von Symfony Messenger
mit class not found ab. Schaltest du eine davon selbst wieder aus, bleibt sie aus.
php.ini
Abschnitt betitelt „php.ini“Die Standardwerte sind für die Entwicklung großzügig, und Fehler werden komplett angezeigt, damit sie dir auffallen:
| Einstellung | Standard |
|---|---|
memory_limit |
256M |
upload_max_filesize |
64M |
post_max_size |
64M |
max_execution_time |
120 |
display_errors |
an |
error_reporting |
E_ALL |
date.timezone |
UTC |
max_input_vars |
5000 |
Bis auf die letzten beiden ist alles ein Feld in der PHP-Karte. max_input_vars liegt höher als die
1000 von PHP, weil große Backend-Formulare (TYPO3, Shop- und Page-Builder-Backends) mehr Felder haben,
und PHP lässt den Rest ohne Fehlermeldung fallen. Die Felder werden dann einfach nicht gespeichert. Das Shopware-Template startet mit einem
memory_limit von 1G, weil Shopware mehr als 256 MB verlangt, und TYPO3 mit 512M, die sein
Install-Tool haben will; die anderen Templates bleiben beim Standard. Ein Projekt, das du über die
API oder das CLI anlegst, bekommt dieselben Standardwerte für alles, was du weglässt.
Ist opcache an, schaut PHP bei jedem Request auf den Zeitstempel der Datei
(opcache.validate_timestamps=1, opcache.revalidate_freq=0). Das kostet etwas Tempo, dafür führt
der nächste Request genau die Datei aus, die du gerade gespeichert hast - und genau das willst du
beim Entwickeln. Außerdem bekommt OPcache mehr Platz als bei PHP üblich (256 MB, 32 MB für Strings,
32531 Dateien), weil ein vendor/-Verzeichnis von Shopware oder Magento die Standardwerte füllt und
OPcache dann bei jedem Request neu kompiliert.
mail() geht an Mailpit, wenn das Projekt es hat; ohne
Mailpit gibt es false zurück.
Werkzeuge im Container
Abschnitt betitelt „Werkzeuge im Container“Neben PHP und Composer bringt das Image mit, was CMS und Frameworks auf einem Server erwarten. WP-CLI
ist als wp da, wp search-replace oder wp plugin list funktionieren also im PHP-Terminal. Die
Kommandozeilen-Werkzeuge von ImageMagick sind für TYPO3 dabei, das Bilder damit skaliert und
zuschneidet statt über die Erweiterung imagick. Der MariaDB-Client (mysql, mysqldump) und der
PostgreSQL-Client der neuesten Hauptversion machen drush sql:*, wp db, artisan db und deine
eigenen Deploy-Skripte möglich. Und weil TYPO3-Site-Konfigurationen eine Locale wie en_US.UTF-8
nennen, sind die UTF-8-Locales für en_US, en_GB, de_DE, de_AT, de_CH, fr_FR, es_ES, it_IT, nl_NL,
pt_BR und pl_PL erzeugt. Daneben findest du git, rsync und msmtp, das mail() an Mailpit übergibt.
Fehlt dir trotzdem etwas, ergänzt es ein eigenes Image, das auf dem von Envoryx aufbaut.
Step-Debugging mit Xdebug
Abschnitt betitelt „Step-Debugging mit Xdebug“Schalte in der PHP-Karte im Bereich Laufzeit Xdebug (Step-Debugging) ein. Envoryx startet Xdebug dann
auf Port 9003 im Modus debug,develop mit start_with_request=yes - jede Anfrage versucht also, deine
IDE zu erreichen.
Xdebug verbindet sich zurück zu dem Rechner, von dem die Anfrage kam. Hinter dem Proxy von Envoryx
findet es diese Adresse in X-Forwarded-For. Klappt das nicht, nimmt es den Entwicklerrechner, den
du unter Einstellungen → Domains & HTTPS → Projekt-Links & Entwicklerrechner einträgst, oder
die Angabe direkt am Projekt, und zuletzt host.docker.internal, den Docker-Host. Das reicht, wenn
PhpStorm auf dem Docker-Host läuft. Deine Workstation ist das aber nicht: Läuft die IDE auf einem
anderen Rechner, den der Server nicht erreicht, leitest du den Port mit dem SSH-Login des Projekts
weiter (ssh -R 9003:localhost:9003) und setzt Debugger-Host (optional) in der PHP-Karte auf
localhost.
In der IDE ordnest du /var/www/html deinem Projektordner zu. Der Bereich IDE zeigt dir die genauen
Einstellungen für PhpStorm und VS Code. Wenn du fertig bist, schalte Xdebug wieder aus - PHP wird damit
spürbar langsamer.
PhpStorm entscheidet über Host und Port, zu welchem seiner Server (und damit zu welchem Pfad-Mapping)
eine Debug-Sitzung gehört. Hinter dem Proxy ist der Port des Browsers 443, nicht die 80, die ein neuer
Server-Eintrag von sich aus bekommt. Passt das nicht, endet die Sitzung im Log mit Cannot bind file
/var/www/html/index.php to the web server project, und deine Breakpoints werden einfach übersprungen.
Envoryx setzt deshalb im PHP-Container PHP_IDE_CONFIG=serverName=<Projekt-Host>, das den Server direkt
benennt, für Anfragen aus dem Browser genauso wie für artisan, drush oder Tests auf der
Kommandozeile. Leg den Server unter Settings → PHP → Servers mit genau diesem Namen an, oder nimm die
php.xml aus dem Bereich IDE, die Name und Port schon richtig enthält. Xdebug ein- oder auszuschalten
startet nur den PHP-Container, seine Worker und den Webserver neu; die Datenbank läuft weiter.
Templates
Abschnitt betitelt „Templates“Laravel, Symfony (Skeleton plus webapp-Pack), WordPress, Drupal mit Drush, TYPO3, Shopware und Craft
CMS. Wie sie funktionieren, steht unter
Projekte anlegen. Wie du die Einrichtung
abschließt, hängt von der Anwendung ab. WordPress hat nur seinen Installer im Browser: Du öffnest die
Seite und trägst dort Titel und Admin-Konto ein. Bei Drupal, TYPO3 und Craft hast du die Wahl
zwischen dem Web-Installer und einer Aktion im Bereich Aktionen; die Aktionen geben am Ende das
Admin-Passwort aus. Die Shopware-Aktion legt den Administrator admin mit dem Passwort shopware
an. Das änderst du am besten gleich nach dem ersten Login im Admin unter /admin.
Das Laravel-Template lässt QUEUE_CONNECTION, CACHE_STORE und SESSION_DRIVER auf database, so
wie Laravels .env sie mitbringt. Envoryx gibt der App die REDIS_*-Variablen, sobald du Redis
hinzufügst, stellt diese drei aber nicht um. Ein Job landet also erst in Redis, wenn du
QUEUE_CONNECTION=redis im Bereich Umgebung oder in der .env setzt (die Extension redis ist schon
an, das hat das Hinzufügen von Redis erledigt). Für eine Laravel-Disk s3 auf dem
Objektspeicher des Projekts installierst du zuerst den
S3-Treiber mit composer require league/flysystem-aws-s3-v3; das Template bringt ihn nicht mit. Sobald das neue Projekt
läuft, führt Envoryx dafür artisan migrate aus. So sind die Tabellen für Jobs, Cache und Sitzungen
da, bevor die erste Anfrage kommt und ein Queue-Worker startet. Legst du das Projekt gestoppt an,
holst du das im Bereich Aktionen nach.
Das Symfony-Template lässt Doctrine seine server_version aus DB_SERVER_VERSION lesen, die Envoryx
für die Datenbanken setzt, die es selbst betreibt. Bei einer rollenden Version wie MariaDB 11 steht
da mariadb-11.0.0, auch wenn der Server schon bei 11.8 ist. Das ist Absicht: So wählt Doctrine eine
Plattform, die der Server sicher kann, und der Wert ändert sich nicht bei jedem Image-Update unter dir
weg. Brauchst du die Features einer späteren Minor-Version, setzt du DB_SERVER_VERSION im Bereich
Umgebung selbst.
Im PHP-Container steckt Node.js 24 mit npm und yarn (pnpm kommt beim ersten Aufruf dazu), also
klappen npm install und npm run build direkt im PHP-Terminal: Vite in einer Laravel-App, ein
Drupal- oder TYPO3-Theme, ein WordPress-Block. Für einen Dev-Server mit Hot Reload schaltest du im
Bereich Laufzeit Node.js aktivieren ein: Node läuft dann in einem eigenen Container auf demselben
Projektverzeichnis, und die npm-Aktionen tauchen im Bereich Aktionen auf. Den Rest findest du auf der
Node.js-Seite.
Laravels Vite-Plugin muss wissen, wo der Dev-Server erreichbar ist, sonst schreibt es
http://0.0.0.0:5173 nach public/hot, und die Seite kommt ohne Styles. Solange der Dev-Server
läuft, bekommt jeder Container des Projekts seine Adresse als ENVORYX_DEV_URL
(https://<projekt>-dev.test), und die vite.config.js des Laravel-Templates nimmt Origin und
Hot-Reload-Socket daraus. In einer eigenen Laravel-App ergänzt du dasselbe im server-Block der
vite.config.js:
server: { ...(process.env.ENVORYX_DEV_URL ? (() => { const dev = new URL(process.env.ENVORYX_DEV_URL); const tls = dev.protocol === 'https:'; return { origin: dev.origin, cors: true, hmr: { host: dev.hostname, protocol: tls ? 'wss' : 'ws', clientPort: Number(dev.port) || (tls ? 443 : 80) } }; })() : {}),},WordPress, Drupal und TYPO3
Abschnitt betitelt „WordPress, Drupal und TYPO3“Diese drei halten einen Teil ihrer Einrichtung in eigenen Konfigurationsdateien, und ein Wert, der dort einmal eingetragen ist, stimmt nicht mehr, sobald das Projekt umbenannt oder dupliziert wird oder einen weiteren Dienst bekommt. Die Templates fügen deshalb ein paar Zeilen hinzu, die stattdessen zur Laufzeit die Variablen lesen, die Envoryx setzt, und eine importierte Website dieser drei bekommt dieselben Zeilen.
WordPress speichert seine eigene Adresse in der Datenbank, und eine kopierte Datenbank würde jeden
Link und den Login von wp-admin an die alte Adresse schicken. Die wp-config.php setzt WP_HOME und
WP_SITEURL deshalb auf die Adresse, über die die Anfrage kam, und ohne Anfrage, etwa bei WP-CLI, auf
die Projekt-URL. Hat das Projekt Redis, setzt sie außerdem WP_REDIS_HOST, WP_REDIS_PORT und, falls
es eins gibt, WP_REDIS_PASSWORD. Object-Cache-Plugins wie Redis Object Cache lesen diese Konstanten
und würden Redis sonst auf 127.0.0.1 suchen; das Plugin selbst installierst du weiterhin selbst.
Drupal bekommt trusted_host_patterns in der settings.php: die Projekt-URL, jeden Namen unter der
Basis-Domain des Projekts, localhost und eine Freigabe über trycloudflare.com. Eine eigene Domain
außerhalb der Basis-Domain braucht ein eigenes Muster unterhalb des Envoryx-Blocks. Dazu kommt eine
drush/drush.yml mit der Projekt-URL, damit drush uli einen Login-Link ausgibt, den du anklicken
kannst, statt http://default/....
Die config/system/additional.php von TYPO3 lässt es dem Proxy vertrauen, der HTTPS beendet, gibt
ihm die Absenderadresse noreply@<projekt-host>, weil sein Mail-Test ohne Absender nichts verschickt,
und schickt Mails per SMTP an Mailpit, solange das Projekt Mailpit hat. Das TYPO3-Template
installiert außerdem die Scheduler-Extension (typo3/cms-scheduler), die in der Basis-Distribution
fehlt, damit der Cron-Job TYPO3 scheduler sofort läuft. Die Aktion typo3 setup fängt wie
drush site:install von vorn an: Sie leert die Projektdatenbank, richtet TYPO3 ein und leert danach
die Caches, damit keine zwischengespeicherte Site-Konfiguration einer früheren Installation die neue
verdeckt.
Shopware
Abschnitt betitelt „Shopware“Ein frisches Shopware-Projekt begrüßt dich mit Shopwares eigenem Web-Installer. Lass ihn links liegen und starte stattdessen system:install –basic-setup im Bereich Aktionen: Das legt in einem Rutsch die Tabellen, einen Verkaufskanal für die Projekt-URL und den Administrator an. Der Web-Installer würde dich dagegen über mehrere Seiten führen und nach dem Datenbank-Passwort aus der Datenbank-Karte fragen.
Shopware erledigt viel über eine Message-Queue und geplante Aufgaben. In einer frischen Installation
arbeitet der Admin die Queue ab, aber nur, solange er in einem Browser-Tab offen ist. Damit das auch
ohne Tab passiert, legst du die Worker Message Queue und Geplante Aufgaben an, wie unter
Worker und Cron-Jobs beschrieben, und schaltest den
Admin-Worker ab. Eine frische Installation hat noch keine config/packages/shopware.yaml, die legst
du mit diesem Inhalt an:
shopware: admin_worker: enable_admin_worker: falseNachdem du ein Plugin oder Theme installiert oder geändert hast, brauchen Administration und Storefront einen neuen Build. Shopwares Skripte dafür rufen abwechselnd PHP und npm auf und laufen deshalb im PHP-Container: Führ build-administration.sh und build-storefront.sh im Bereich Aktionen aus.
Hat das Projekt OpenSearch, findet Shopware es über
OPENSEARCH_URL schon von selbst. Benutzt wird es aber erst, wenn du im Bereich Umgebung
SHOPWARE_ES_ENABLED=1 und SHOPWARE_ES_INDEXING_ENABLED=1 setzt und den Index einmal mit
php bin/console es:index im PHP-Terminal aufbaust. Mit Redis ist es ähnlich: Shopware nimmt es
nicht, nur weil es da ist. Damit Symfonys App-Cache dort landet, legst du
config/packages/redis.yaml an:
framework: cache: app: cache.adapter.redis default_redis_provider: '%env(REDIS_URL)%'Aktionen, Tests und Worker
Abschnitt betitelt „Aktionen, Tests und Worker“Im Bereich Aktionen liegen die Befehle, die du sonst im Terminal tippen würdest: composer install,
update und dump-autoload; für Laravel artisan migrate, migrate:fresh --seed, db:seed,
key:generate, storage:link, cache:clear und optimize:clear; für Symfony cache:clear und
doctrine:migrations:migrate; drush site:install und cache:rebuild; TYPO3 setup; Shopware
system:install, build-administration.sh und build-storefront.sh; craft install; dazu php -v und php -m. Ausführen kannst du eine
Aktion nur, wenn das Projekt die passenden Dateien hat: Ein Symfony-Projekt listet auch die
Artisan-Aktionen, aber ausgegraut, mit artisan wurde im Projekt nicht gefunden daneben.
PHPUnit und Pest werden automatisch erkannt und laufen im Bereich Tests mit JUnit-Report, sodass du siehst,
welcher Test fehlschlägt; ist Pest installiert, bekommst du nur die Pest-Suite, die die PHPUnit-Tests
mit ausführt. Sie laufen gegen <datenbank>_test, nicht gegen deine Entwicklungsdatenbank:
Envoryx richtet DB_DATABASE, DB_URL und DATABASE_URL dorthin und legt die Datenbank vor dem Lauf
an. Die übrigen <env>-Einträge der phpunit.xml gelten wie auf deinem Laptop, QUEUE_CONNECTION=sync
und MAIL_MAILER=array von dort schlagen also, was das Projekt für die Entwicklung setzt. Warum, und was das Doctrine-Recipe von Symfony damit zu tun hat, steht unter
Aktionen und Tests.
Für alles, was dauerhaft im Hintergrund laufen soll, gibt es Worker: Laravel-Scheduler, Queue-Worker, Queue-Listener, Horizon und Reverb, Symfony Messenger und Scheduler, die Message Queue und die geplanten Aufgaben von Shopware, die Queue von Craft sowie beliebige PHP- und Composer-Skripte. Das Formular startet mit dem, der zu deinem Framework passt. Details unter Worker und Cron-Jobs.
Dateien und Eigentümer
Abschnitt betitelt „Dateien und Eigentümer“Das Projektverzeichnis ist unter /var/www/html eingebunden. Was PHP dort schreibt, gehört
PUID:PGID - du kannst die Dateien also weiter vom Host oder per SMB bearbeiten, ohne Ärger mit
Rechten. Die Downloads von Composer landen im Paket-Cache des
Projekts, ein zweites composer install lädt sie also nicht noch einmal. Schaltest du dort das
Teilen ein, ist das zweite Laravel-Projekt deutlich schneller eingerichtet als das erste.