Shopware Container Deployment: Warum das Image read-only bleibt

|

Ein Container-Image ist wie ein Polaroid: Nach dem Auslöser wird nichts mehr übermalt. Was das für Shopware bei mittwald heißt, erfährst du in diesem Beitrag.

Container-Image bei Shopware: Warum es sich nicht mehr ändert

Ich sammle Polaroid-Kameras. Was ich an ihnen liebe, ist ihre Endgültigkeit. du drückst den Auslöser, das Bild entwickelt sich, fertig. Nachschärfen geht nicht. Wenn du ein anderes Bild willst, musst du ein neues knipsen.

Ein Container-Image funktioniert genauso, das sagt schon der Name. Es ist ein Abbild deiner Anwendung, das beim Build entsteht und danach unverändert bleibt. Theoretisch war mir das klar, richtig verstanden habe ich es erst, als ich Shopware bei mittwald deployen wollte. Dabei ist mir eine Annahme aufgefallen, die ich jahrelang mit mir herumgetragen hatte: Ich dachte immer, dass ich Dateien schreiben darf.

Lass uns die kleinen Hürden abhaken, die das Image für mittwald braucht. Danach kommt die Frage, um die es eigentlich geht: Was muss aufs Bild, und was passiert danach drumherum?

Shopware Docker-Recipe: Was composer require shopware/docker liefert – und was fehlt

Shopware bringt ein eigenes Docker-Image mit. Mit dem Befehl composer require shopware/docker legst du ein docker/Dockerfile ins Projekt. Außerdem bekommst du die Infrastruktur-Services für Datenbank, OpenSearch und Mailer. Shopware empfiehlt diesen Weg auch, so übernimmt ein Paket-Update auch die Änderungen am Dockerfile aus dem Upstream.

Was das Recipe nicht liefert, ist ein laufender Anwendungscontainer. Das Dockerfile liegt dort und die Services drumherum starten, aber niemand baut daraus ein Image und bringt es auf die Plattform. Diese Lücke füllen wir jetzt zusammen.

Shopware-Image bauen: TLS und Architektur vorher klären

Das Shopware-Image nutzt FrankenPHP, und in FrankenPHP steckt Caddy. Caddy kümmert sich beim Start ungefragt um HTTPS, auf mittwald terminiert aber schon der Ingress das TLS. Zwei wollen also dieselbe Aufgabe übernehmen, und eine davon hat für ihre Zertifikate nicht einmal Schreibrechte. Eine Umgebungsvariable stellt das ab:

CADDY_GLOBAL_OPTIONS=auto_https off

Mit dieser Variable läuft das offizielle Standard-Image auf mittwald, ohne dass sich Caddy selbstständig ums TLS kümmern muss. So brauchst du dazu keine eigene Caddyfile und musst somit das Dockerfile auch nicht ändern. Die mitgelieferte Caddyfile liest die Variable automatisch aus, du musst sie also nur setzen. Das Image lauscht außerdem bereits auf Port 8000.

Es hat nicht lange gedauert, bis ich in die nächste Hürde gelaufen bin, denn mittwald-Container laufen auf linux/amd64. Wenn du wie ich auf einem Apple-Silicon-Rechner nativ baust, bekommst du ein arm64-Image. Beim Deployment erscheint dann die Meldung: Only linux/amd64 supported.
Ein sozusagen "zweigleisiges" Vorgehen ist hier sinnvoll. Für den lokalen Test baust du nativ, hier läuft dieser Build ohne Emulation. Für das Deployment baust du ein eigenes amd64-Image mit Buildx. Dieser Build läuft auf Apple Silicon und dauert daher deutlich länger.

user@local $ docker buildx build \
  --platform linux/amd64 \
  -f docker/Dockerfile \
  --build-arg PHP_VERSION=8.3 \
  -t docker.p-XXXXXX.project.space/shopware/shopware-app:8.3-frankenphp \
  --push \
  .

Das Build-Argument PHP_VERSION pinnt das Base-Image, damit dir die PHP-Version zwischen zwei Builds nicht unter den Füßen wegdriftet. Und das ist es fast schon soweit! 


Dann, bevor du pushst, mach einen lokalen Durchlauf. Stell im app-Service in Deiner compose.yaml das lokale Image ein. Meist gibt es Probleme mit dem Environment. Dort muss CADDY_GLOBAL_OPTIONS: auto_https off stehen. Sonst startet Caddy wieder mit seiner eigenen CA. Danach initialisierst du Shopware im Container.

user@local $ docker compose exec app vendor/bin/shopware-deployment-helper run

Der Deployment-Helper prüft den Zustand der Datenbank. So erkennt er selbst, ob eine Installation oder eine Aktualisierung nötig ist. Startet der Shop in diesem Schritt, hast du die meisten Fehler schon gesehen. Solange du noch leicht darauf zugreifen kannst, bist du auf der sicheren Seite.

Shopware Volumes: Welche Laufzeitdaten persistent sein müssen

Beim Containerisieren stellt sich jedes Mal dieselbe Frage, ganz gleich ob es um Shopware, WordPress oder eine eigene Anwendung geht: Wo liegen die Bewegungsdaten, und was ist fix?

Fix ist alles, was aus dem Quellcode entsteht. Das steckt im Image. Bewegungsdaten entstehen zur Laufzeit und brauchen persistente Volumes, sonst sind sie beim nächsten Deployment weg. So weit die bekannte Zweiteilung. Bei Shopware sind es aber drei Kategorien, und die dritte ist die unangenehme.

Also zurück zu Shopware: Ein Teil der Laufzeitdaten ist im Shopware-Fall abgeleitet. Das sieht so aus: Die kompilierten Theme-Dateien liegen unter public/theme, die Thumbnails unter public/thumbnail und die Sitemap unter public/sitemap liegen im Volume, weil Shopware sie zur Laufzeit schreibt. Gehen sie verloren, kostet Dich das drei Befehle und etwas Geduld:

user@shop $ bin/console theme:compile
user@shop $ bin/console media:generate-thumbnails
user@shop $ bin/console sitemap:generate

Der Rest hat kein Negativ, wenn wir in der Polaroid Analogie bleiben. Ein Produktbild unter public/media hat jemand hochgeladen, die Rechnungsdokumente unter files sind Belege. Beides kannst bzw. darfst Du nicht neu rendern, und darin sind diese Dateien ihren analogen Verwandten ähnlich: ein Polaroid ohne Negativ, weg ist weg.

Das ergibt das folgende Bild:

Für unseren Shopware-Fall bzw. die Mounts heißt das zwei Gruppen statt einer Liste. Regenerierbar sind die folgenden:

/var/www/html/public/theme
/var/www/html/public/thumbnail
/var/www/html/public/sitemap

Und unersetzlich diese hier:
 

/var/www/html/public/media
/var/www/html/files

Ein Volume ist noch kein Backup. Und selbst wenn eines läuft, kommt beim Zurückholen die unangenehme Frage nach dem Zeitpunkt: Die Produktdaten liegen in der Datenbank, die Bilder liegen im Volume, und gesichert wird beides von verschiedenen Mechanismen. Holst du die zwei aus unterschiedlichen Momenten zurück, hast du Produkte ohne Bilder oder Bilder ohne Produkte. Das Album und das Verzeichnis müssen denselben Stand haben. Sieh also nach, was bei deinen Volumes gesichert wird und wie oft, bevor der erste echte Shop darauf läuft. Und wenn dir die Antwort nicht gefällt: public/media und files sind die beiden Pfade, bei denen man über externen Object Storage nachdenken könnte.

Somit ein kleiner Hint: Falls du diese Dateien gar nicht im Container haben möchtest, empfiehlt Shopware stattdessen externen S3-kompatiblen Object Storage. Wenn du das public-Filesystem entsprechend einstellst, werden Theme-, Asset- und Sitemap-Dateien dorthin verschoben. Dann brauchst du keine Volumes. 

Warum der Shopware Extension Store im Container nicht funktioniert

Letzte Sonderlocke im Shopware-Fall: Der Extension Store will eine Extension installieren und schreibt dafür in public/bundles. Das ist Teil des read-only Images und kein Volume, soll auch keins sein.

Unable to create a directory at bundles/swagextensionstore.
mkdir(): Permission denied

Ausgerechnet der Store, der Extensions verwalten soll, kann sich hier nicht selbst installieren.

Mein erster Gedanke war, das als Einschränkung der Plattform zu sehen. Das stimmt aber nicht. Shopwares eigener Guide zum Extension-Management rät dazu, Extensions über Composer zu installieren. Man soll das nicht während des Betriebs über die Administration machen. Der Container sorgt also nur dafür, dass man das tut, was in der Dokumentation ohnehin steht. 

Damit Shopware es gar nicht erst versucht und den funktionslosen Store im Admin ausblendet, kommt eine Konfigurationsdatei ins Projekt und damit zur Build-Zeit ins Image:

shopware:
  deployment:
    runtime_extension_management: false

Diese Einstellung gilt erst ab Shopware 6.6.4.0. Ab dann kommen Extensions über composer require hinzu. Sie werden wie jede andere Abhängigkeit committed.

Fazit: Ein Image ist wie ein Polaroid

Jede Fehlermeldung auf dem Weg war irgendwo eine Variante desselben Satzes: Caddy wollte Zertifikate schreiben. Der Extension Store wollte Bundles schreiben. theme:create wollte Quellcode schreiben. Drei sehr unterschiedliche Symptome, doch im Edneffekt dieselbe Ursache. Was aufs Bild muss, entscheidest Du beim Build. Was danach entsteht, gehört ins Album, und ein Teil davon hat kein Negativ.

Im Developer Portal sind der gesamte Weg mit allen Schritten sowie jede Fehlermeldung mit ihrer Ursache aufgeführt. Wenn du das Ganze danach nicht mehr von Hand machen willst, zeigt dir der Guide zum Container-Deployment mit GitHub Actions, wie Build und Push in eine Pipeline integriert werden. Da geht es beim nächsten Mal weiter.

Ähnliche Artikel:

Container auf dem Root-Server? Was managed Hosting dir abnimmt

Container 101 (4): Container auf dem Root Server? Was Managed Hosting dir abnimmt

Container auf dem Root Server oder managed Hosting? Wir zeigen dir, wo deine Zeit hinfließt und was mittwald dir bei Sicherheit, Updates und Backups abnimmt.

Container 101 (3): Volumes, Umgebungsvariablen & Portfreigaben

Basiswissen für Agenturen: Wie Volumes Daten erhalten, Umgebungsvariablen die Konfiguration liefern und Portfreigaben den Container erreichbar machen.

Rust Token Killer (RTK): Token-Optimierung für Claude Code, Cursor & Co.

Bis zu 90 % weniger Token im KI-Kontext: So macht RTK Deine Coding-Agents schlanker – und kompatibel mit dem mittwald AI Hosting.

wp2shell: Als der Support-Posteingang wegen einer WordPress Sicherheitslücke explodierte

Mitte Juli explodierte unser Support-Posteingang: Die Fakten zur WordPress Sicherheitslücke wp2shell und wie sie sich bei mittwald bemerkbar machte.

Container 101 (2): So hängen Image und Registry zusammen

Basiswissen für Agenturen: Wie Docker-Images entstehen, wo sie gespeichert werden und wie Deployment darauf aufbaut.

Kommentar hinzufügen