Kompetenzen Portfolio Referenzen Lebenslauf Blog Termin buchen

Die perfekte lokale PHP-Entwicklungsumgebung

Gepostet von: Felix Dziekan in: Blog am 

Das habe ich am Ende meines Artikels darüber, warum ich DDEV nicht nutze, versprochen: Wenn ich keine vorgefertigte Umgebung will, was fahre ich dann?

Hier ist es. Nichts Exotisches, keine magische CLI, nur Docker und eine Handvoll Entscheidungen, für die ich ein paar Jahre und einige kaputte Laptops gebraucht habe.

Die Regel, aus der alles andere folgt

Nichts wird auf dem Host installiert. Nie. Außer der IDE.

Kein PHP auf dem Laptop. Kein Composer, kein Node, kein MySQL, kein Redis. In dem Moment, in dem du eines davon "nur mal eben" installierst, hast du angefangen, eine Umgebung zu bauen, die es nur auf deiner Maschine gibt, und du wirst die nächsten zwei Jahre Kollegen erklären, warum ihr Setup sich anders verhält.

Alles lebt in Containern. Der Host hält Quellcode, eine IDE und Docker. Das ist der ganze Vertrag.

Quellcode und IDE bleiben auf dem Host; alles, was Code ausführt, lebt in einem Container

Die Services

Ein typisches Projekt von mir hat drei Services. Nicht weil drei eine magische Zahl wäre, sondern weil es so viele separate Dinge sind, die es zum Laufen wirklich braucht.

 

services:
  db:
    build:
      context: ./env/base/DB/
    ports:
      - "3306:3306"
    container_name: myproject_db_dev

  php:
    build:
      context: ./env/dev/Php
    volumes:
      - ./project:/var/www/site/
    environment:
      - PHP_IDE_CONFIG=serverName=myproject
    links:
      - "db:database"
    container_name: myproject_php_dev

  web:
    image: serie3/prodweb:20.4
    volumes:
      - ./project:/var/www/site/
    links:
      - "db:database"
      - "php:php"
    ports:
      - "80:80"
    container_name: myproject_web_dev

 

Drei Dinge daran sind erwähnenswert.

Der web-Container ist das Produktions-Image. Kein Doppelgänger, derselbe Tag, der auf dem Server läuft. Wenn sich nginx lokal seltsam verhält, verhält es sich in Produktion auch seltsam, und das will ich an einem Dienstagnachmittag herausfinden und nicht während eines Releases.

Der Quellcode wird gemountet, nicht kopiert. ./project:/var/www/site/ heißt, der Container liest die Dateien, die ich gerade bearbeite. Kein Rebuild, kein Sync-Werkzeug, kein Watcher. Datei speichern, Seite neu laden.

Benannte Container. container_name: myproject_php_dev kostet nichts und rettet dich jeden Tag, denn docker exec -it myproject_php_dev sh kannst du aus dem Kopf tippen und docker exec -it project-php-1 sh nicht.

Das Dev-PHP-Image ist das Prod-Image plus Debugging

Das ist das Stück, das die ganze Einrichtungsmühe wert macht. Das Entwicklungs-Dockerfile ist vier Zeilen lang, weil es mit dem Image anfängt, das schon in Produktion läuft:

 

FROM serie3/prodphp:8.2

RUN apk add --no-cache linux-headers libzip-dev \
    && apk add --update --no-cache --virtual .build-dependencies $PHPIZE_DEPS \
    && pecl install xdebug-3.2.0 \
    && docker-php-ext-install zip

COPY xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini

 

Gleiche PHP-Version, gleiche Extensions, gleiche INI-Defaults — plus Xdebug. Es gibt kein Auseinanderlaufen zu erklären, weil es keine zweite Umgebung gibt, von der etwas auseinanderlaufen könnte.

Xdebug, der Teil, an dem alle hängenbleiben

Xdebug in einem Container hat genau ein hartes Problem: Der Debugger läuft im Container, die IDE auf dem Host, und der Container hat keine Ahnung, wo der Host ist. Das ist die Konfiguration, die das löst:

 

zend_extension=xdebug.so
xdebug.mode=debug
xdebug.client_port=9003
xdebug.max_nesting_level=400
xdebug.discover_client_host=on
xdebug.client_host=host.docker.internal
xdebug.start_with_request=yes

 

discover_client_host=on lässt Xdebug die Host-Adresse aus dem eingehenden Request ermitteln, und host.docker.internal ist der Rückfall, wenn es das nicht kann. Port 9003 ist der Standard von Xdebug 3 — wenn du einem älteren Tutorial folgst, das 9000 sagt, ist das Xdebug 2, und es wird sich still nie verbinden.

start_with_request=yes heißt, dass jeder Request versucht, einen Debugger zu erreichen. Manche hassen das und stellen es auf trigger. Ich lasse es an: Wenn meine IDE nicht lauscht, passiert nichts, und wenn sie lauscht, trifft mein Breakpoint, ohne dass ich an eine Browser-Erweiterung denken muss.

Die andere Hälfte des Puzzles ist PHP_IDE_CONFIG=serverName=myproject in docker-compose. Dieser String muss zum Servernamen passen, den du in PhpStorm konfigurierst, und so weiß die IDE, welches Path Mapping sie anwenden soll. Mach ihn falsch, und du bekommst das frustrierendste Symptom der Welt: Der Debugger verbindet sich und hält an gar nichts an.

Mail: einfangen, nie senden

Niemand sollte aus einer Entwicklungsumgebung eine echte E-Mail senden können. Nicht "wir passen auf", nicht "die Adressen sind erfunden" — niemand. Nimm einen Mailcatcher dazu und zeig PHP darauf:

 

  mail:
    image: sj26/mailcatcher
    ports:
      - "1080:1080"

 

Dann setz MAILER_DSN=smtp://mail:1025 und öffne localhost, um alles zu lesen, was die Anwendung zu senden glaubt. Jede Registrierungsmail, jedes Passwort-Zurücksetzen, jede Benachrichtigung — sichtbar, inspizierbar, und geht nirgendwohin.

Der SSH-Key, ohne ihn ins Image zu backen

Früher oder später braucht ein Build ein privates Repository, und damit deinen SSH-Key. Es gibt einen sehr verlockenden falschen Weg:

 

# mach das nicht
ARG SSH_PRIVATE_KEY
RUN echo "${SSH_PRIVATE_KEY}" > /root/.ssh/id_rsa

 

Dieser Key liegt jetzt in einer Schicht des Images. Ihn in einem späteren Schritt zu löschen entfernt ihn nicht — jeder, der das Image ziehen kann, liest ihn aus der Schichtenhistorie zurück.

Lokal mountest du den Key stattdessen zur Laufzeit, schreibgeschützt:

 

  php:
    volumes:
      - ./project:/var/www/site/
      - ~/.ssh/id_rsa:/root/.ssh/id_rsa:ro

 

Und in CI nimmst du BuildKit-Secrets, die den Key für einen einzelnen RUN einhängen und nichts zurücklassen:

 

RUN --mount=type=ssh composer install --no-dev

 

Was ich nicht in die Umgebung packe

Keine IDE-Konfiguration im Repository außer einem gemeinsamen Code-Style. Kein "führ dieses Bash-Skript aus, bevor du anfängst". Keine Abhängigkeit von einem bestimmten Host-Betriebssystem. Das Setup muss eine Kollegin auf macOS überleben, eine Kollegin auf Windows mit WSL, und mich auf Linux, ohne eine einzige Fallunterscheidung.

Der Test dafür, ob eine Umgebung fertig ist, ist einfach: Gib das Repository jemandem, der es noch nie gesehen hat, und schau, ob docker compose up plus ein Importbefehl zu einer funktionierenden Seite führt. Wenn es mehr braucht, ist die Umgebung nicht fertig — sie ist nur dokumentiert.

Wie es weitergeht

Zwei Dinge bauen direkt auf diesem Setup auf: PHPUnit aus PhpStorm heraus durch den Container laufen zu lassen, und die Handvoll Dinge, die ich in jedes PHP-Projekt packen würde, bevor eine Zeile Anwendungscode entsteht.

Sie brauchen einen Senior-Entwickler, der liefert?

Über 20 Jahre. Produktionssysteme. Echte Deadlines. Ob Architekt, Entwickler oder DevOps-Engineer — ich habe alle drei Rollen übernommen, oft im selben Projekt. Sparen wir uns den Agentur-Overhead und sprechen direkt.