Zum Inhalt springen

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.

Die Karte PHP im Bereich Laufzeit: PHP-Version, memory_limit, Upload- und Laufzeitgrenzen, error_reporting und die Erweiterungen zum EinschaltenDie Karte PHP im Bereich Laufzeit: PHP-Version, memory_limit, Upload- und Laufzeitgrenzen, error_reporting und die Erweiterungen zum Einschalten

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.

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.

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.

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.

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.

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.

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.

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) } };
})() : {}),
},

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.

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

Nachdem 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)%'

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.

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.