Mach jedes Repository preview-fähig — liefere eine build-basierte docker-compose.preview.yml (läuft auf jedem Runner-Host) und eine .spedy/preview.yml, die Service und Port deklariert, die Spedy nach außen routet.
BYOC = Bring Your Own Compose. Eine Preview bootet die eigene Compose-Datei deines Projekts — kein Shopware-/Magento-Template. Du sagst Spedy nur, welcher Service die Preview ausliefert; der Supervisor baut + startet deinen Stack und routet ihn auf eine Pro-Ticket-URL.
Damit ein Repository als Pro-Ticket-Live-Preview nutzbar ist, brauchst du zwei Dinge im Repo:
eine Compose-Datei, die deinen Stack startet — idealerweise eine dedizierte, build-basierte docker-compose.preview.yml (siehe unten), und
eine .spedy/preview.yml, die Service + Port deklariert, die Spedy nach außen routet.
Diese Seite deckt beides ab, plus die Anforderungen an den Runner-Host, damit die Preview auf deiner eigenen Infrastruktur tatsächlich hochkommt. Die UI-Seite — Repo verbinden, DB-Snapshots hochladen, Preview im Ticket nutzen — findest du unter Preview-Umgebungen.
dein-repo/├── docker-compose.preview.yml # der Preview-Stack (empfohlen — siehe unten)├── docker-compose.yml # dein normaler Dev-Stack (Fallback)├── .spedy/│ └── preview.yml # welcher Service/Port ist die Preview?└── … # dein App-Code
.spedy/preview.yml:
# Sagt dem Spedy-Supervisor, wie diese Preview exponiert wird.service: app # Name des Compose-Service, den Traefik routetport: 3000 # Container-Port, der HTTP ausliefert
Wenn du auf einem Ticket „Preview starten" klickst, macht der Runner:
klont das Repo (auf dem Feature-Branch des Tickets),
wählt die Compose-Datei (bevorzugt docker-compose.preview.yml — siehe Welche Compose-Datei),
liest .spedy/preview.yml,
baut + bootet den Stack und routet service/port unter <orgSlug>--<ticket>.preview.<deine-domain>.
Eine dedizierte docker-compose.preview.yml gewinnt also immer vor deiner Dev-Compose. Liefere eine — das ist der Unterschied zwischen einer Preview, die überall läuft, und einer, die nur läuft, wenn dein Host exakt richtig konfiguriert ist (siehe Zwei Wege, die Source bereitzustellen).
Der Supervisor führt das docker compose up der Preview gegen den Host-Docker-Daemon aus (die Preview-Container sind Geschwister des Supervisors, nicht in ihm verschachtelt). Diese eine Tatsache entscheidet, wie deine Source in die Container gelangen muss.
Liefere eine docker-compose.preview.yml, die jedes App-Image baut und die Source per Build-Context ins Image backt. Der Build-Context wird vom Docker-Client (im Supervisor) gelesen und an den Daemon gestreamt — er hängt also von keinem Host-Pfad ab. Diese Preview läuft auf jedem Runner-Host ohne zusätzliche Host-Konfiguration.
# docker-compose.preview.ymlservices: app: build: context: . # Inline-Dockerfile — keine separate Datei nötig. Backt die Source ein. dockerfile_inline: | FROM node:22-alpine WORKDIR /app RUN corepack enable COPY . /app # Runtime installiert Deps + startet den Dev-Server, damit alles, was zur # Laufzeit aus der Umgebung gelesen wird (URLs, Feature-Flags), funktioniert. command: sh -c "corepack prepare pnpm@latest --activate && pnpm install && pnpm dev" environment: - NODE_ENV=development depends_on: - db db: image: postgres:16 environment: POSTGRES_PASSWORD: postgres volumes: - db_data:/var/lib/postgresql/datavolumes: db_data:
# .spedy/preview.ymlservice: appport: 3000
Hinweise:
Keine Host-Bind-Mounts der Source. Genau das ist der Punkt — nichts unter volumes: sollte ein Host-Pfad sein (./src:/app). Named Volumes (wie db_data) sind in Ordnung.
Ein Service, der zur Build-Zeit ein Geschwisterverzeichnis braucht (z. B. ein gemeinsames openapi/ eine Ebene höher), holt es mit build.additional_contexts:
Behalte den Runtime-Befehl (pnpm dev / next dev / vite), damit umgebungsabhängige Konfiguration zur Laufzeit gelesen wird (nicht beim Build eingefroren) und du beim Iterieren Hot Reload hast. Bei Frameworks, die Public-Env zur Build-Zeit ins Bundle backen (z. B. Next.js NEXT_PUBLIC_*), erspart der Dev-Server das Durchreichen jedes Werts als Build-Arg. Auf einem kleinen Runner-Host kann der Dev-Server einer schweren App bei On-demand-Compiles OOM-gekillt werden; falls das passiert, führe stattdessen einen Production-Build im Befehl aus (pnpm install && pnpm build && pnpm start, NODE_ENV=production) für einen stabilen, speicherärmeren Preview.
Wenn du die Preview einfach auf deine normale Dev-docker-compose.yml zeigen lässt (die Source bind-mountet, z. B. ./api:/app), gibt es keinen Build-Schritt — aber die Bind-Quelle wird auf dem Host aufgelöst. Der Klon muss also auf einem Host-Pfad liegen, den der Daemon sieht, identisch innerhalb und außerhalb des Supervisor-Containers. Siehe Anforderungen an den Runner-Host. Fehlt dieser Mount, bekommst du ein leeres /app und einen Crash-Loop:
ERR_PNPM_NO_PKG_MANIFEST No package.json found in /app
Bevorzuge das build-basierte Modell, außer du brauchst explizit Live-Editing vom Host.
Zwei sofort anpassbare Vorlagen — die zwei Dateien ins Repo-Root legen, Pfade/Versionen anpassen, committen, Repo verbinden. Beide nutzen das empfohlene build-basierte Modell und laufen daher auf jedem Runner-Host.
shopware-preview.zip herunterladen — basiert auf dem Community-Image dockware/dev (PHP + nginx + MySQL + Shopware-CLI), mit eingebackenem Custom-Code und Theme-Compile / Cache-Clear als Post-Run-Hooks.
# docker-compose.preview.ymlservices: shopware: build: context: . dockerfile_inline: | FROM dockware/dev:6.6.10.3 # An dein Repo-Layout anpassen (Plugins, Themes, Config). COPY --chown=www-data:www-data custom/plugins /var/www/html/custom/plugins environment: - APP_ENV=dev
Das All-in-one-dockware-Image betreibt sein eigenes MySQL und bietet keinen initdb-Hook — ein hochgeladener DB-Snapshot kann dort also nicht automatisch eingespielt werden. Für Snapshots einen dedizierten mysql-Service betreiben, Shopware darauf zeigen lassen und db.service: db setzen — die Dateien im Zip zeigen beides auskommentiert.
Name des Compose-Service (Schlüssel unter services:), mit dem Traefik spricht. Weglassen → Auto-Detection (siehe unten).
port
empfohlen
Container-interner Port, auf dem dein Service HTTP ausliefert (z. B. 3000, 8080, 80).
domain
optional
Eine zusätzliche Host()-Regel neben <slug>.preview.<deine-domain> — z. B. eine feste Prod-Preview-Domain.
db.service
optional
Der Compose-Service, in den ein DB-Snapshot restauriert wird (gemountet in dessen /docker-entrypoint-initdb.d/). Nur nötig bei DB-Snapshots.
setup
optional
Shell-Befehle, die nachcompose up der Reihe nach im Primary-Container laufen (z. B. Migrationen, Seeds). Ein Nicht-Null-Schritt lässt den Boot fehlschlagen.
views
optional
Navigierbare Einstiegspunkte als Toggle in der Preview-UI (je label + path).
hooks
optional
Im Repo deklarierte Befehle, die der Preview-Agent rund um jeden Run ausführt: hooks.pre_agent (davor) und hooks.post_agent (danach). Das Repo-Äquivalent der Pre-/Post-Run-Befehle in der UI. Jeder Eintrag ist ein einfacher String oder ein { run, service, timeout }-Mapping (Timeout in Sekunden, Default 300, max. 1800).
hot_reload
optional
true überspringt den Image-Rebuild nach Agent-Edits — für Dev-Stacks, die die Repo-Source bind-mounten und selbst hot-reloaden (Nuxt/Vite/Next dev). Für build-basierte Previews (Source zur Build-Zeit eingebacken) weglassen — der Supervisor erkennt den häufigen Bind-Mount-Fall automatisch.
.spedy/preview.yaml (mit a) wird ebenfalls akzeptiert.
Wichtig:port ist der Port im Container, kein Host-Mapping. Dein Service muss im Container darauf lauschen (0.0.0.0:<port>, nicht 127.0.0.1). Host-Port-Mappings (ports:) in deiner Compose werden von Spedy entfernt — parallele Previews würden sonst kollidieren — du brauchst sie für die Preview nicht.
Diese gelten für den Host, der den Spedy-Supervisor betreibt (deinen selbst-gehosteten Runner). Das build-basierte Modell braucht nur Punkt 1–2; das Bind-Mount-Modell zusätzlich Punkt 3.
Docker-Socket. Der Supervisor steuert den Host-Daemon, um Preview-Stacks zu booten. Mounte ihn in den Supervisor-Service:
Ein Preview-Ingress-Traefik im geteilten Netz. Previews werden an ein externes Docker-Netz namens spedy-preview mit Traefik-Routing-Labels gehängt; ein Traefik in diesem Netz macht aus dem Pro-Ticket-Host eine Route. Der Supervisor legt das Netz bei Bedarf an, aber der Ingress-Traefik muss ihm beitreten:
Ohne ihn läuft Preview-Traffic ins Leere (127.0.0.1:9880) und der Browser bekommt 404.
Geteilter Klon-Workspace — nur fürs Bind-Mount-Modell. Da Bind-Quellen host-seitig aufgelöst werden, muss der Klon-Root des Supervisors ein Host-Bind-Mount mit identischem Pfad innen und außen sein (passend zur preview.workspace-Einstellung des Supervisors), z. B.:
volumes: - /srv/spedy-previews:/srv/spedy-previews # gleicher Pfad auf Host und im Container
Das build-basierte Modell braucht das nicht — lass es ganz weg, wenn alle deine Previews ihre Images bauen.
Lass eine Preview niemals deine Runner-Infrastruktur booten. Wenn dieselbe Compose-Datei, die deinen Supervisor / Ingress-Traefik definiert, auch das ist, was eine Preview bootet, würde ein nacktes compose up diese innerhalb der Preview starten und ihr den Host-Docker-Socket geben — ein Container-Escape. Halte Preview-Infra aus der Datei, die die Preview nutzt: liefere eine separate docker-compose.preview.yml mit nur deinem App-Stack (das empfohlene Modell), oder gate die Infra-Services hinter einem Compose-profiles:, damit compose up sie überspringt.
Beim Boot rendert der Supervisor eine abgeleitete docker-compose.spedy.yml neben deiner Compose-Datei (damit relative build/context-Pfade weiter zum Repo auflösen). Dabei:
entfernt er ports: (Host-Mappings) und container_name an allen Services (Kollisionen über parallele Previews),
härtet er jeden Service (entfernt privileged, cap_add, Host-devices, security_opt, Host-Namespace-Sharing und unsichere Bind-Mounts wie /var/run/docker.sock oder .. — die Compose ist angreifer-beeinflusst),
hängt er den Primary-Service ans externe spedy-preview-Netz,
injiziert er Traefik-Labels (Routing zu <slug>.preview.<deine-domain>, loadbalancer.server.port = dein port),
mountet er den geklonten Workspace nach /opt/spedy-workspace im Primary-Container (wo der Coding-Agent arbeitet).
Die generierte docker-compose.spedy.yml wird lokal via .git/info/exclude ignoriert — sie landet nie in deinem Repo/PR.
Nachdem der Coding-Agent den Workspace bearbeitet hat, erscheinen die Änderungen so in der Preview:
Build-basierte Preview: der Supervisor baut das Image nach einem Agent-Lauf neu (--build), sodass die neue Source eingebacken und ausgeliefert wird. Zuverlässig auf jedem Host.
Bind-Mount-Preview mit Dev-Server: läuft dein Stack im Watch-Modus (next dev / node --watch / vite) von der gemounteten Source, erscheinen Änderungen sofort ohne Rebuild.
.spedy/preview.yml kann mehr als nur Service und Port deklarieren.
Datenbank-Snapshots werden in der Produkt-UI hochgeladen und ausgewählt, nicht ins Repo committet — siehe Preview-Umgebungen → Datenbank-Snapshot hochladen. Beim Boot lädt der Supervisor den aufgelösten Snapshot als .spedy/snapshot.sql.gz neben deinen Klon (git-ignoriert, wird nie committet) und mountet ihn in /docker-entrypoint-initdb.d/ des Service, den du unter db.service nennst — das Postgres-/MySQL-/MariaDB-Image spielt ihn beim ersten Boot ein. Das Repo deklariert also nur, welcher Service die Datenbank ist:
db: service: db # der Compose-Service, in den der Snapshot restauriert wird
setup: führt Shell-Befehle einmalig, der Reihe nach, im Primary-Container direkt nach compose up aus — Migrationen, Seeds, ein Asset-Build. Ein Nicht-Null-Schritt lässt den Boot fehlschlagen.
hooks.pre_agent / hooks.post_agent laufen rund um jeden Preview-Agent-Run — das Repo-Äquivalent der Pre-/Post-Run-Befehle unter Projekt → Agent-Einrichtung → Preview-Agent. Jeder Eintrag ist ein einfacher String oder ein { run, service, timeout }-Mapping:
ERR_PNPM_NO_PKG_MANIFEST: No package.json found in /app (leeres /app)
Bind-Mount-Modell auf einem Host ohne geteilten Klon-Workspace → entweder auf eine build-basierte docker-compose.preview.yml umsteigen, oder den Workspace-Mount am Supervisor ergänzen (Punkt 3 oben).
ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: node:sqlite (Crash-Loop)
Das Node im Image ist zu alt für das gepinnte pnpm (pnpm ≥ 11 braucht Node ≥ 22.13). Pinne ein kompatibles pnpm (corepack prepare [email protected]) oder nutze ein node:22-Base-Image.
Der Supervisor / Ingress-Traefik taucht innerhalb der Container-Liste einer Preview auf
Die Preview hat eine Compose-Datei gebootet, die deine Runner-Infra enthält → liefere eine separate docker-compose.preview.yml mit nur dem App-Stack, oder gate die Infra hinter profiles:.
Start bricht ab: „could not identify a primary HTTP service"
Kein Service mit Port + keine .spedy/preview.yml → service + port deklarieren.
Preview lädt, ist aber 502 / leer
Dein Service lauscht nicht auf dem in port angegebenen Container-Port, oder bindet 127.0.0.1 statt 0.0.0.0.
Preview-Host liefert 404
Kein Preview-Ingress-Traefik im spedy-preview-Netz auf dem Runner-Host (Punkt 2 oben).
Der falsche Service wird geroutet
Auto-Detection hat den falschen gewählt → service explizit setzen, oder labels: { spedy.primary: "true" } ergänzen.
docker-compose.spedy.yml taucht in git status auf
Sie sollte via .git/info/exclude ignoriert sein; wenn nicht, Preview einmal neu starten (der Supervisor ergänzt den Eintrag beim Boot).