Der ultimative Guide zur Web-Entwicklung mit Docker Desktop (Windows)

Wer meine Beiträge eifrig verfolgt (Hallo, Mama!), weiß, dass alles auf Linux, bzw. Ubuntu, basiert. Web-Entwicklung unter Windows ist natürlich möglich, aber u.U. aufwändiger.

Ein Hauptproblem ist z.B. die Versionierung: Benötigt man für ein Projekt PHP 4.2 (das gute alte “Oh, das war immer schon so”-System), steht man schön doof da, wenn man es parallel zu seiner vorhandenen PHP-Umgebung installieren möchte. Das gleiche gilt, wenn man neben MySQL noch MongoDB, Neo4J, NodeJS, Kibana und Shnurblr benötigt.

Abhilfe schafft eine virtuelle Maschine, die sich z.B. mit VirtualBox schnell installieren und hochfahren lässt. Auch hier gibt es natürlich einige kleinere Probleme, die einem das Leben schwer machen. Hat man die aber aus dem Weg geräumt, lässt es sich sehr gut damit arbeiten.

Aufgeräumter entwickelt man aber, wenn man Docker nutzt. Wie das geht, habe ich bereits mal kurz erklärt – aber eben nur für Linux.

Was du hier lernen könntest

Wie man letztendlich ein Projekt programmiert, ist Geschmackssache und es gibt viele Wege, die zum Ziel führen. Dieses Tutorial zielt darauf ab, ein bisschen mit Docker zu arbeiten und ein paar Stellen zu beleuchten, die man irgendwann mal brauchen könnte. Das ein oder andere scheint vielleicht zu aufwändig oder unnötig zu sein, aber nur so merkt man, welche persönliche Präferenz man selbst hat.

Wenn es so klingt, als sei das eine faule Ausrede, weil ich etwas falsch erklärt oder ausgeführt habe… Erwischt.

Bevor es los geht, denke aber daran: Es gibt einen Unterschied zwischen deiner Entwicklungsumgebung (“Dev”) und den Live-Servern (“Prod”)! Sicherheit kann für diesen Artikel hinten an stehen; sobald du aber mit deiner Anwendung öffentlich gehst, musst du selbst entscheiden, ob du Docker, Kubernetes oder Bare-Metal nutzen willst und – vor allem – wie du es schützt (Stromkabel kurz vor einem Hackerangriff rausziehen ist keine Option).

Die Krux mit Windows

Genau wie Lord Voldemort mit seinem Horkruxen zu kämpfen hatte, so leiden alle ein bisschen unter Windows. Zum Glück hat sich einiges verbessert, denn seit einiger Zeit kann ein eigenständiges Linux nebenher laufen. Das nennt sich WSL – Windows-Subsystem für Linux – und kommt jetzt in der Version 2 daher.

Muss man sich also keine virtuelle Maschine erstellen? Ist jetzt alles Click&Develop? Tja… nicht ganz. Zu aller erst müssen wir überlegen, was wir wollen. Das ist generell ein ganz guter Tipp fürs Leben!

Die schönste Entwicklungsumgebung

Wie sieht unsere Traum-Entwicklungsumgebung aus? Übrigens auch ein super Thema für ein erstes Date.

  • Wir nutzen Docker, bzw. Docker-Files, um die Server zu erstellen. Wir haben damit nur ein paar Dateien, die man ausführen muss, um die Entwicklungsumgebung zu erstellen. Die muss man noch nicht mal verstehen!
  • Wir nutzen Docker-Desktop für Windows und hoffen, dass es, im Gegensatz zu einem Terminal-Linux, schönes Klickibunti gibt.
  • Wir wollen alle Quell-Dateien in einem beliebigen Verzeichnis speichern, so dass wir mit Windows einfach darauf zugreifen können.
  • Alles soll so konfiguriert sein, dass man es schnell auf einen anderen Rechner übertragen kann.
  • Außerdem natürlich: Es muss flott genug sein.

Und hier beginnen die Probleme… Denn: Grundsätzlich hat Windows Probleme damit, eine große Menge an Dateien mit WSL zu teilen, jedenfalls dann, wenn es schnell gehen muss. Dieser GitHub-Thread quatscht schon seit 2019 darüber (Spoiler: Vielleicht hat sich das mit Windows 11 erledigt).

Die Probleme

Leider ist es bei PHP, bzw. Symfony (Laravel und NodeJS Anwendungen fallen auch darunter) nun mal so, dass tausende kleine Dateien mitmischen. Nach etlichen Tests kann ich sagen: Symfony-Umgebungen, deren Dateien in einem Windows-Verzeichnis liegen und die mit einem Linux-System gekoppelt werden, sind ätzend langsam. Das ist schon bei VirtualBox der Fall, wenn das Verzeichnis per Sharing eingebunden wird.

Was heißt langsam? Dabei kann es sich bei einem einfachen Seitenaufruf (ohne Datenbank) von ca. 1 Sekunde handeln. Klingt vielleicht nicht viel, nervt aber gewaltig.

Voraussetzung ist also, dass die Source-Dateien im gleichen Linux-System liegen wie die PHP-Umgebung. Zugriff auf diese Dateien erhalten wir dann z.B. über einen Netzwerkzugriff. Spoiler vorweg: Das klappt mit WSL und Docker Desktop sehr gut. Datei-Berechtigungen sind übrigens kaum ein Problem, da alle Docker-Container über root laufen.

Wenn man seine Projekte allerdings alle auf einer externen Festplatte haben möchte, um transportabel zu sein und diese dann an andere Rechner anzuschließen, muss man ein bisschen in die Trickkiste greifen. Die Lösung: Das externe Image, das alle Daten von Docker Windows beinhaltet und nur eine VHDX-Datei ist, wird einfach anstatt im Windows-Benutzer-Verzeichnis auf ein anderes Laufwerk gespeichert.

Vorteil gegenüber VirtualBox?

Was ist jetzt der Vorteil, Windows im Gegensatz zu einer virtuellen Linux-Maschine zu nutzen?

Letztendlich ist es vielleicht doch eine Glaubensfrage. Beides muss man installieren und beides hat Hürden, die, einmal überwunden, gar nicht so schlimm sind. Letztendlich wollen wir aber doch alle das Gleiche: Eine einfache, über Text-Dateien konfigurierte Umgebung, so dass der Quell-Code und vor allem der Server einfach überall läuft.

Thema heute also: Wie mache ich das mit Docker unter Windows?

Was wir genau vorhaben

Ich komme mal direkt zum Ergebnis, ohne an die Sache didaktisch ran zuführen. Glaub mir einfach, so ist das eine gute Lösung. Oder auch nicht. Kann aber sein. Wasweißich.

Wir erstellen…

  • Ein Docker-Konfigurationsprojekt in einem “normalen” Verzeichnis unter Windows: Dieses Projekt definiert, welche Container/Server wir haben wollen.
    • Ein Nginx-Webserver
    • Eine PHP-Umgebung
    • Eine MySQL-Datenbank
    • Eine Linux-Shell für Zeugs (z.B. Deployment oder Webpack)
  • Ein Docker-Volume, das alle unsere ausführbaren Projekt-Dateien enthält (also den PHP-Code): Das ist wichtig, denn ein Docker-Volume wird direkt in den Container eingebunden und ist bedeutend schneller, als wenn man ein Windows-Verzeichnis einbindet. Das Docker-Volume ist ein bisschen versteckt, lässt sich aber genauso aufrufen wie alle anderen Directories auch.
  • Eine Visual-Code-Umgebung, die direkt auf der Linux-Shell zuhause ist und auch deren PHP-Implementation benutzt: Das nennt sich VS Code Remote Development.

Zusammengefasst: Docker Windows erstellt seine eigene Linux-Umgebung, die per WSL 2 eingebunden wird. Wir können von Windows aus auf die Linux-Dateien zugreifen (und auch anders herum). Damit alles schnell läuft, speichern wir den Programm-Code auf das Linux-System, die Dateien, die die Docker-Umgebung definieren, aber im Windows-System.

Schritt 1: Docker-Desktop installieren

Als erstes solltest du dafür sorgen, dass Docker Desktop auf deinem Windows läuft. Dazu benötigst du WSL 2 (Windows Subsystem Linux; sorgt dafür, dass du ein Linux in Windows laufen lassen kannst) und lässt sich wie hier beschrieben installieren.

Meine Zusammenfassung dazu:

  • Öffne die PowerShell als Administrator.
  • Führe die folgenden zwei Befehle aus:
    • dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
    • dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
  • Starte Windows neu.
  • Lade das Update-Paket für WSL herunter und installiere es.
  • Führe folgenden Befehl in der PowerShell aus:
    • wsl --set-default-version 2

Das war es aber schon. Jetzt kannst du Docker Desktop für Windows installieren! Anschließend öffnest du die Windows PowerShell und siehst mit dem Befehl wsl -l die installierten Distributionen:

Das setzt natürlich voraus, dass Docker bei dir auch läuft. Das siehst du am Icon in der Status-Leiste. Und da haben wir auch unser Klickibunti!

Hier kannst du später deine Container/Server einfach starten und Daten einsehen. Das gleiche wäre natürlich auch auf einem Linux-Terminal möglich, dann aber in Textform.

Zusammengefasst: Wir können die Docker-Container über das Windows-Fenster (ja, doppelt gemoppelt) starten und stoppen. Die eigentliche CLI für die Bedienung auf der Shell ist aber auch verfügbar.

Schritt 2: Auf Daten des Docker-Systems zugreifen

Du hast Zugriff auf das ganze Linux-System, das Docker heimlich installiert, mit deinem Datei-Explorer. Gib einfach folgende Adresse ein: \\wsl$

Hier findest du, genau wie beim PowerShell-Befehl vorhin, die Verzeichnisse docker-desktop und docker-desktop-data. Im zweiten finden wir später unsere Dateien, mit denen wir arbeiten.

Schritt 3: Ein Docker-Volume erstellen

OK… Hier müssen wir leider ein bisschen vorgreifen. Als nächstes erstellen wir ein Docker-Volume. Das ist letztendlich ein eigener kleiner Container, der unsere Daten, bzw. den Source-Code unserer Projekte beinhalten wird. Wir können Docker später höflich sagen, dass er dieses Docker-Volume bitte in alle Container als Verzeichnis einbinden soll.

Alternativ kannst du auch ein Windows-Verzeichnis benutzen. Das macht aber aus den o.a. Performance-Gründen keinen Sinn – es sei denn, du hast nur ein kleines Python-Projekt. Deshalb brauchen wir jetzt ein Volume, das direkt in das Linux-Subsystem eingebettet ist.

Ein Volume erstellt du ganz einfach in der PowerShell mit folgendem Befehl:

docker volume create <NAME>

Was solltest du als Name nehmen? Für mich macht es Sinn, Volumes nach Firmen zu ordnen. So habe ich z.B. ein privates Volume und jeweils eins für meine Kunden (z.B. “private-volume”, “swoplo-volume”). “volume” muss eigentlich nicht in den Namen, aber das macht die Verwirrung hinterher geringer.

Mit dem ls-Befehl kannst du dir deine Volumes anzeigen lassen. Im Klickibunti-Fenster geht das mittlerweile auch (linke Seite, “Volumes”).

Zwischentipp: Docker-Volumes Datei auf externen Datenträger verschieben

Docker speichert seine Volumes (desktop-docker-data) in eine VHDX-Datei im Verzeichnis C:\Users\<USER>\AppData\Local\Docker\wsl\data. Möchtest du das nicht, weil du z.B. einen externen Datenträger benutzt, kannst du diese in der Windows-Powershell verschieben (Achtung: Verzeichnisse anpassen!):

# WSL herunterfahren
wsl --shutdown

# Alle Container anschauen
wsl -l -v

# Container exportieren
wsl --export docker-desktop-data X:\temp\export-data.tar

# Container deregistrieren
wsl --unregister docker-desktop-data

# Container wieder importieren
wsl --import docker-desktop-data X:\da_wo_es_liegen_soll X:\temp\export-data.tar --version 2

Tipp: Die VHDX-Datei wächst ständig an und wird immer fetter. Löschst du etwas im Docker-Volume, bleibt die Datei genauso groß.

Wenn du Windows 10 Pro hast, kannst du diese mit folgenden Befehlen verkleinern:

wsl --shutdown
Optimize-VHD -Path <Pfad>/ext4.vhdx -Mode Full

Kein Windows 10 Pro? Kein Problem, geht auch so:

wsl --shutdown
diskpart
select vdisk file="<Pfad>/ext4.vhdx"
attach vdisk readonly
compact vdisk
detach vdisk
exit

Wenn du dich fragst, warum die VHDX-Datei trotzdem noch so riesengroß ist: Docker speichert im Ordner \\wsl$\docker-desktop-data\version-pack-data\community\docker\overlay2 gecachte Objekte zur Erstellung der Images. Die kannst du mit docker system prune löschen. Achtung: da dieser Befehl mit seinen Optionen ALLES löschen kann (auch Volumes), solltest du die Container starten, die du “schützen” möchtest.

# Entfernt alle gestoppten Container und ungenutzten Images und Caches
docker system prune

# Entfernt wirklich alles, was gerade nicht gestartet ist
docker system prune --all --volumes

Schritt 4: Visual Studio Code Plugins installieren

Starte Visual Studio Code und stelle sicher, dass du folgende Plugins installiert hast:

  • Docker: Gibt dir einen Tab mit einer Übersicht über alle Docker-Container, Images, Volumes, etc.
  • Remote Development: Loggt dich in eine fremde Maschine ein, auf der du dann arbeitest.

Schritt 5: Docker-Projekt anlegen

Es geht ans Eingemachte! Wir legen jetzt ein Docker-Projekt an, das alle Server und Verzeichnisse definiert, die wir für die Entwicklung benötigen. Starte also mit einem leeren Projektverzeichnis (in einem Windows-Ordner), das du in VS-Code öffnest.

Ich habe es “symfonytest-docker” genannt. Das Projekt selbst wird “symfonytest” heißen. Macht Sinn, oder?

Die Verzeichnisstruktur

Für ein Docker-Projekt brauchen wir (als Beispiel) folgende Dateien:

  • .env: Das Environment-File; hier finden sich Umgebungsvariablen, die sich von Installation zu Installation unterscheiden können (z.B. Projektnamen, Ports, Versionen, etc.).
  • docker-compose.yml: Das ist die Hauptdatei, aus der Docker die einzelnen Container starten wird (hier eine genaue Aufstellung). Nicht alle Informationen sind darin enthalten, denn es gibt…
  • Weitere Verzeichnisse: Hier finden sich Dateien, Configs, etc. für die jeweiligen Container; Nginx benötigt seine eigenen Dateien, usw.

Schritt 6.1: Ein einfacher Webserver zum Warmwerden

Code auf GitHub: https://github.com/bjoernfal/docker-tutorial/tree/01-nginx

OK, beginnen wir mit was Einfachem, dem Nginx-Webserver. Schauen wir uns die docker-compose.yml mal an:

Das sagt nichts anderes aus als:

  • Docker-Compose-Datei ist in Version 3.9 geschrieben (Referenz hier)
  • Es gibt einen Nginx-Container, das Image dazu ist das neueste, kleinste (“alpine”).
  • Der Port 80 des Containers soll 1234 auf dem Host sein

Du startest die Umgebung, indem du auf das Docker-File rechts klickst und “Compose Up” wählst.

Im Terminal sieht es dann so aus:

Diese Befehle hättest du übrigens auch in der PowerShell eingeben können… Aber warum, wenn es so einfach ist. “Compose Restart” erstellt übrigens alle deine Container neu.

Anschließend läuft dein Webserver! Schau mal in das Docker-Fenster:

Und im Browser unter http://localhost:1234:

Cool! Läuft!

Hinweis: Dir werden häufig “alpine”-Versionen von Images begegnen. Diese Images basieren auf dem besonders kleinen Alpine-Linux, das du auch selbst für deine eigenen Images nutzen kannst.

Schritt 6.2: Ein bisschen Config

Jetzt legen wir eine .env-Datei an, in der wir ein bisschen rumkonfigurieren:

  • Eine feste Nginx-Version (anstatt die “Neueste”)
  • Der Port für den Host
  • Der Container-Name

Die Dateien dazu findest du hier: https://github.com/bjoernfal/docker-tutorial/tree/02-env

Es sollte selbsterklärend sein, dass der Port und die Nginx-Version jetzt in der .env-Datei ausgelagert sind. Aber der Container-Name wurde angepasst, sowie der Projektname:

COMPOSE_PROJECT_NAME wird intern von Docker benutzt und macht zusammen mit container_name die Umgebung nochmal ein bisschen übersichtlicher:

Wir erinnern uns: Der Projektname war vorhin “symfonytest-docker” (den Zusatz “docker” brauchen wir hier aber nicht) und der Nginx-Container wird nicht mehr durchnummeriert. Es macht durchaus Sinn, manchmal keinen Container-Namen zu setzen, aber bei der Entwicklung ist es einfacher, später Zugriff darauf zu bekommen (der Domainname ist nämlich der Container-Name).

Außerdem nutzen wir den Projektnamen im Container-Namen: ${COMPOSE_PROJECT_NAME}_nginx. Das umgeht das Problem, dass ein Container immer eindeutig identifiziert werden muss.

Tipp: .env-Dateien beinhalten alle Parameter, die sich von Maschine zu Maschine unterscheiden. In Linux werden diese Parameter Umgebungsvariablen genannt und z.B. mit dem Befehl “export <VARIABLE>=<WERT>” gespeichert.
Du kannst diese aber auch in einem Dockerfile über ARG setzen. Doch Vorsicht: Diese Umgebungsvariablen sind nur in der Docker-Shell verfügbar! Solltest du dich später mal per SSH in den Container einloggen wollen, sind diese nicht da. Speichere sie deshalb z.B. in der Datei /etc/environment.

Schritt 6.3a: Eine PHP-Datei im Volume erstellen

Bevor wir uns in die Höhle der Möwen wagen und uns zukacken lassen, erstellen wir ein PHP-File, das wir ausführen wollen. Dazu müssen wir, wie gesagt, ein Projekt in unserem Volume anlegen.

Dazu öffnest du folgendes Verzeichnis: \\wsl$\docker-desktop-data\version-pack-data\community\docker\volumes\private-volume\_data (“private-volume” durch dein Volume ersetzen).

Hier erstellst du das Projekt-Verzeichnis, in unserem Fall also phptest. Lege dort eine index.php-Datei deiner Wahl an, wie z.B.

<?php

echo "Hallo Welt!";

Das _data-Verzeichnis, bzw. Volume kannst du später in all deine Container einbinden. Du kannst natürlich auch ein Volume nur für dieses Projekt anlegen, ganz wie du magst. In diesem Beispiel nehmen wir aber an, das Volume beinhaltet immer mehrere Projekte.

Schritt 6.3: PHP, Dockerfiles und Volumes

Jetzt geht’s richtig rund. Wir installieren eine PHP-Umgebung, konfigurieren Nginx und legen fest, wo unser Quell-Code gespeichert wird.

Den Code dazu findest du hier: https://github.com/bjoernfal/docker-tutorial/tree/03-php

OK… Jetzt gibt es eine Menge zu verdauen. Schauen wir mal, was alles angepasst wurde:

  • .env: Ein paar Variablen sind dazu gekommen, unter anderem die PHP-Version und das Volume, das unsere Projekte enthält.
  • docker-compose.yml: Wir haben jetzt zwei Container, Nginx und PHP-FPM. Ohne ins Detail zu gehen: Nginx benötigt den PHP-Container und bindet diesen per “Upstream” ein.
  • nginx: localhost wurde in localhost.conf so konfiguriert, dass das Root-Verzeichnis /var/projects/phptest genutzt wird.

Die wichtigste Neuerung ist wohl das Einbinden von “Dockerfiles”. Diese befinden sich in den jeweiligen Verzeichnissen nginx und php-fpm. Schauen wir uns mal eins an:

Das sind Befehle, die Docker dazu bringen, bestimmte Dinge zu installieren. Als erstes übernehmen wir das Argument PHP_VERSION aus der env-Datei (Achtung: Das muss auch in docker-compose.yml übergeben werden). Dann legen wir im FROM-Teil das Image fest. Weitere PHP-Module werden über den RUN-Befehl installiert. Anschließend wird der php-fpm-Command gestartet, damit der Container nicht sofort ausgeht.

Diese Verzeichnisse können auch andere Dateien beinhalten. Nginx hat da eine ganze Menge:

Alle diese Config-Dateien werden in der docker-compose.yml als Verzeichnis eingebunden:

Das erleichtert es, den Webserver zu konfigurieren. Einfach die Datei ändern und den Container neu starten. Hier siehst du auch, dass das Projekt-Volume als /var/projects eingebunden wird.

Übrigens, den Container kannst du ganz einfach über das Docker-Fenster neu starten:

Wenn du alles richtig gemacht hast, ist die PHP-Datei erreichbar.

Ein paar kurze Anmerkungen zu PHP, dem Docker-Image und PHP-Extensions

In unserem Fall nutzen wir eine besondere Version, nämlich FPM, vom PHP-Docker-Image. Hier gibt es einige Extensions, die bereits installiert sind. Wenn du wissen willst welche, logge dich in die Docker-Maschine ein. Das geht ganz komfortabel über diesen Knopf:

Anschließend öffnet sich eine Shell, in der du mit php -m alle installierten Extensions einsehen kannst.

In dieses Dockerfile habe ich bereits die MySQL-Extension eingeschmuggelt:

Weitere Infos zur Installation von PHP-Modulen findest du auf der Docker-Seite.

Einbinden des Project-Volumes

Damit Nginx auch unsere PHP-Datei findet, haben wir das Verzeichnis /var/projects auf das project-volume gesetzt.

Aber Achtung, jetzt nicht verwirrt sein! project-volume bezieht sich in diesem Fall auf den Namen des Volumes innerhalb der docker-compose.yml. Erst weiter unten sagen wir, um welches Volume es sich wirklich handelt – nämlich genau dem, das wir in .env definiert haben.

Tipp: Wenn du wissen willst, mit welchen Modulen php-fpm daher kommt, schau einfach in das Dockerfile auf GitHub!

Schritt 6.4: Eine Development-Shell installieren

Jetzt brauchen wir noch eine Linux-Shell mit SSH-Zugriff, mit der wir ein paar Konsolen-Befehle ausführen können. Es macht Sinn, diese auch mit Docker zu erstellen, da wir einiges damit vorhaben:

  • Symfony installieren (über den Installer)
  • Webpack Encore laufen lassen
  • Deployment ausführen
  • Als Remote Development Maschine in VS-Code nutzen
  • SSH-Zugang mit SSH-Key (Bonus)
  • und noch viel mehr (z.B. bei Freunden angeben)

Moment“, sagst du, “warum erstellen wir nicht eine einzige virtuelle Maschine mit PHP, MySQL und all dem Gedöns?“. Gute Frage! Natürlich können wir ein Dockerfile entwerfen, das alles in einer Maschine vereint. Die einzelnen Systeme sollen aber in ihrer eigenen Instanz laufen, damit sie schön voneinander getrennt sind.

OK, wo waren wir? Ja, richtig. Wir bauen eine Shell, die uns bei der Entwicklung helfen wird. Dazu gibt es wieder Code: https://github.com/bjoernfal/docker-tutorial/tree/04-shell

Hier liegt die ganze Magie im shell/Dockerfile. Die Schritte sind:

  • Basis-Image festlegen (Ubuntu)
  • Argumente setzen (SSH-Key, PHP-Version)
  • SSH Key aus shell-Verzeichnis in .ssh kopieren (theoretisch könnte man hier auch ein Volume verlinken)
  • Basis-Pakete installieren (Curl, Git, etc.)
  • Alles auf Deutsch setzen
  • Repos/Installationen vorbereiten (z.B. Yarn oder PHP)
  • Diese Pakete installieren
  • Als Bonus: SSH-Login nur über SSH-Key ermöglichen (indem die sshd_config kopiert wird)

Eine Kleinigkeit noch zum docker-compose.yml File:

hostname vergibt dem Container einen – ja richtig – Hostname. Ansonsten wäre es eine beliebige, unschöne Zeichenfolge.

Also, du kannst dich jetzt mit Putty/Pageant und dem Key aus dem Verzeichnis shell einloggen über SSH einloggen (Port 5022, also root@localhost:5022)! Das geht übrigens auch über diesen Knopf hier:

Aber wenn wir später Encore nutzen wollen, macht es Sinn, einen direkten SSH-Zugang zu haben. Vielleicht ist es auch einfach nur Geschmackssache. Mach wie du willst 🙂

Du hast dabei folgende Möglichkeiten, die Shell zu erreichen:

  • Putty (root@localhost:5022)
  • Ein Terminal/Bash über VS-Code (das geht besonders einfach, wenn du Remote Development nutzt, siehe Schritt 6.7)
  • Über die CLI von Docker-Desktop
  • Hardcore über einen Docker-Befehl in der Windows-PowerShell (Docker Desktop macht nichts anderes)

Dazu gibst du einfach folgenden Befehl ein (geht nicht nur über die PowerShell, sondern auch über das Terminal in VS-Code)

# Shell /bin/sh
docker exec -it symfonytest_shell /bin/sh

# Shell /bin/bash
docker exec -it symfonytest_shell /bin/bash

Schritt 6.5: Symfony installieren

Wir installieren jetzt mal ein Framework mit vielen, vielen kleinen Dateien. Dazu gibt es neuen Code! https://github.com/bjoernfal/docker-tutorial/tree/05-symfony

  • Schaue dir die Änderungen in shell/Dockerfile an. Hier gibt es ein paar neue PHP-Module, die wir zum Ausführen der Symfony-Installation benötigen.
  • Die Nginx-Config wurde angepasst, so dass das Symfony-Projekt direkt läuft.
  • Passe in der .env deine Git-Einstellungen an (GIT_EMAIL und GIT_USER). Das benötigt die Symfony-CLI, sonst gibt es Fehler beim initialisieren des Git-Repositories durch den Symfony-Befehl.

Logge dich jetzt in die Shell-Maschine ein (entweder Putty, über Docker oder über VS-Code) und führe folgende Befehle aus:

# Symfony installieren
cd /var/projects
symfony new symfonytest --full

So sollte das Ergebnis aussehen:

Und im Browser unter http://localhost:1234 sollte die Seite erreichbar sein:

Eine Kleinigkeit zur Verzeichnisfreigabe/Docker-Benutzer

Das Cache, bzw. Log-Directory, für Symfony benötigt natürlich Schreibrechte für den Webserver (wie eigentlich jede PHP-Applikation). Wenn du dir das erstellte Verzeichnis anguckst, siehst du, dass var auch auf chmod 777 gestellt ist.

Das ist nicht grad so knolle. Für die Entwicklungsumgebung ist es nicht so tragisch, aber ein guter Zeitpunkt, um etwas über das Benutzer-Management von Docker zu erfahren. Schauen wir uns doch mal an, welcher Nutzer hier eine Log-Datei erstellt hat:

Aha. User “82”. Wer ist das denn? Das ist die User-ID, die PHP-FPM für den User www-data benutzt.

In diese Bredoullie kommen wir deshalb, weil wir ein Verzeichnis mit drei verschiedenen Containern teilen. In einer produktiven Umgebung solltest du die Berechtigungen also dementsprechend anpassen.

Schritt 6.6: MySQL hinzufügen

Jetzt fügen wir MySQL hinzu (OK, eigentlich MariaDB). Hier der Code dazu: https://github.com/bjoernfal/docker-tutorial/tree/06-mysql

Dieses Mal haben wir kein Dockerfile, sondern konfigurieren alles in der Compose-Datei.

Wichtige Anmerkung hierzu: Alles, was dynamische Daten beinhaltet (also die Datenbank selbst), sollten wir außerhalb des Containers speichern. Ansonsten werden deine Daten beim nächsten Erstellen der Umgebung einfach zerschreddert.

Du kannst dazu ein eigenes Volume benutzen oder, wie in diesem Fall, die Dateien in das Verzeichnis var auf deiner Hostmaschine (also da, wo die Docker-Dateien liegen) speichern lassen. Das wird häufiger vorkommen, deshalb macht es Sinn, zunächst den Containernamen (also mysql) als Verzeichnis zu erstellen und dort dann alle Dateien zu speichern. Hier benutzen wir sogar die Version im Verzeichnisnamen, damit es später zu keinem Konflikt kommt.

Möchtest du dein gesamtes System auf ein anderes übertragen, nimm var einfach mit!

Noch eine sehr wichtige Anmerkung: Der Einfachheit halber ist das Root-Passwort für MySQL in der Environment-Datei gespeichert. Das ist natürlich generell keine gute Idee – bei der Entwicklung u.U. aber nicht so wichtig. Stattdessen solltest du Docker Secrets benutzen.

MySQL Zugang testen

Wie können wir uns jetzt in die Datenbank einloggen? Hier ist folgendes zu beachten:

  • Per Default ist im Docker-Image bind-address ausgeschaltet. Das bedeutet, dass der Zugang von außen nicht geblockt wird! Für unsere Zwecke ist das praktisch – aber auf keinen Fall in einer Live-Umgebung. Solltest du also vorher anpassen.
  • Innerhalb unserer Docker-Umgebung gilt der Container-Name als Domain-Name. In diesem Fall können die Container also mit den Namen mysql, nginx, etc. untereinander kommunizieren.

Testen wir den Zugang doch mal in unserer Shell (da wurde mysql-client als Paket hinzugefügt):

Nicht verwirrt sein: Das mysql hinter -h steht für den Datenbank-Server.

Quick-Tipp: Wenn du die Host-Maschine innerhalb eines Containers ansprechen will, kannst du den Domain-Namen host.docker.internal benutzen (Infos dazu hier).

Intermezzo: Die vielen Ports

Vielleicht fragst du dich jetzt, wie du dir die vielen Ports merken sollst. Leider ist es so, dass jeder einzelne Container seinen eigenen Port auf deinem Host benötigt und diese sich auch von Projekt zu Projekt unterscheiden. Du fragst dich bestimmt auch, wie deine Webseite später mal erreichbar sein soll, wenn der Docker-Container auf einem anderen Port als dem üblichen 443 läuft…

Die Antwort: Ein “Reverse Proxy”. Das ist praktisch Nginx, der alle Anfragen von außen auf den üblichen HTTP-Ports (80/443) entgegennimmt und nach Domain auf die Docker-Container verteilt. Hier findest du ein gutes Tutorial dazu. Das ist ein wichtiges Detail, wenn du später ein Orchestrierungs-Tool wie Kubernetes nutzen möchtest.

Schritt 6.7: VS-Code Remote Development

Jetzt könntest du deinen Source-Code natürlich ganz einfach bearbeiten, indem du auf die \\wsl$-Ressource zugreifst und direkt das Docker-Volume ansprichst.

Geht aber auch viel cooler. Mit Visual Studio Remote-Development verbindest du dich mit dem Shell-Container und nutzt dessen komplette Installation (d.h. z.B. PHP-Version).

Das geht folgendermaßen: Öffne den Remote-Explorer und wähle den entsprechenden Container aus.

Anschließend wird VS-Code etwas in diesen Container installieren:

Und zum Schluss hast du vollen Zugriff auf das Dateisystem der Maschine und kannst das entsprechende Verzeichnis auswählen:

Unten links kannst du sehen, dass du mit dem Container verbunden bist.

Weitere Vorteile von VS-Code und Docker

Es gibt noch ein bisschen mehr, wie dir VS-Code unter die Arme greift und deine Achselhöhlen ankitzelt.

Eine interessante Anwendung ist die Datei devContainer.json. Hier kannst du ein Dockerfile definieren, das VS-Code automatisch erkennt, einen Container baut, sich damit verbindet und sogar die entsprechenden Extensions installiert. Infos dazu findest du in diesem Blog oder auch bei Microsoft selbst.

Dabei ist es sogar möglich, unsere docker-compose-Struktur direkt in den Quellcode einzubinden!

Projekt am nächsten Tag öffnen

Morgen hast du bestimmt wieder alles vergessen (ich auch, deshalb schreibe ich das hier). Deshalb eine kurze Erinnerung, wie du die Entwicklungsumgebung wieder öffnest:

  • Docker Desktop starten (wenn das nicht schon mit dem Windows-Start passiert)
  • Die Container starten, in dem du auf den Play-Button für deine Umgebung drückst
  • Das Projekt “/var/projects/symfonytest [Container symfonytest_shell (shell)]” unter “File > Recent” öffnen
  • Alternativ: Das Projekt-Verzeichnis unter \wsl$\docker-desktop-data\version-pack-data\community\docker\volumes\<Dein Volume\_data öffnen

Docker Compose im Terminal einsetzen

Es sei erwähnt, dass du “Compose Up” nicht nur anklicken, sondern auch über das Terminal starten kannst.

In diesem Fall ist --env-file und -f docker-compose.yml eigentlich unnötig; aber solltest du später einmal eine docker-compose-Datei für Prod aufsetzen (die übrigens nach der ersten geladen werden kann und aufeinander aufbauen), musst du diese angeben.

Aber Achtung: Nicht immer wird Docker Compose den Container neu zusammenstellen, besonders, wenn du etwas am Dockerfile verändert hast. Nutze dann folgenden Befehl:

docker-compose build <ServiceName>

ServiceName ist in diesem Fall der Name des Services in der docker-compose.yml (also “shell”, “php-fpm”, usw.).

Wie anfangs erwähnt, solltest du deine Entwicklungsumgebung auf keinen Fall 1:1 auf einen Live-Server übernehmen! Dafür bedarf es einiger weiterer Absicherungen (auf die wir jetzt nicht eingehen).

Einsatz im Live-/Produktivsystem

Du musst auch überlegen, ob es für dich Sinn macht, überhaupt Docker auf einem Live-System einzusetzen. Der Vorteil ist die einfache Verwaltung von Diensten, d.h. du schreibst einmal dein Docker-Compose-File und kannst so mehrere unabhängige Instanzen starten. Der Nachteil: Deine Umgebung ist begrenzt auf einen Server. Dafür gibt es aber “Orchestrierungen” wie Kubernetes, die Docker-Container (oder andere) auf viele Server verteilen.

Fazit

Es gibt sehr viele Wege, eine einfache Entwicklungsumgebung aufzubauen. Möchtest du diese aber mit anderen teilen, lohnt sich der Blick auf Docker und sogar devcontainer-Dateien. Für umfangreichere Projekte, die viele verschiedene Anwendungen benötigen, ist es eine große Erleichterung!

Björn Falszewski
14. Juli 2021
Disclaimer
Alle meine Artikel entstehen mit bestem Wissen und Gewissen, sind aber nicht perfekt und sollten immer nur als Ausgangspunkt für deine eigenen Recherchen bilden.

Sollte dir etwas Fehlerhaftes auffallen, freue ich mich über deine Nachricht!