SpedySpedy Docs

Preview-Konfiguration (BYOC)

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:

  1. eine Compose-Datei, die deinen Stack startet — idealerweise eine dedizierte, build-basierte docker-compose.preview.yml (siehe unten), und
  2. 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.

Minimales Setup

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 routet
port: 3000       # Container-Port, der HTTP ausliefert

Wenn du auf einem Ticket „Preview starten" klickst, macht der Runner:

  1. klont das Repo (auf dem Feature-Branch des Tickets),
  2. wählt die Compose-Datei (bevorzugt docker-compose.preview.yml — siehe Welche Compose-Datei),
  3. liest .spedy/preview.yml,
  4. baut + bootet den Stack und routet service/port unter <orgSlug>--<ticket>.preview.<deine-domain>.

Welche Compose-Datei wird genutzt

Der Supervisor sucht eine Compose-Datei in dieser Reihenfolge und nimmt die erste, die er findet:

docker-compose.preview.yml
docker-compose.preview.yaml
compose.preview.yml
compose.preview.yaml
docker-compose.yml
docker-compose.yaml
compose.yml
compose.yaml

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).

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.

Empfohlen — das Image bauen (host-unabhängig)

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.yml
services:
  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/data

volumes:
  db_data:
# .spedy/preview.yml
service: app
port: 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:
    build:
      context: ./web
      additional_contexts:
        shared: ./shared
      dockerfile_inline: |
        FROM node:22-alpine
        WORKDIR /app
        COPY . /app
        COPY --from=shared . /shared
  • 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.

Alternative — Source bind-mounten (braucht Host-Setup)

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.

Framework-Beispiele

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.

Nuxt

nuxt-preview.zip herunterladen — ein einzelner Nuxt-Service, der die Source einbäckt und den Dev-Server auf 0.0.0.0:3000 startet.

# docker-compose.preview.yml
services:
  app:
    build:
      context: .
      dockerfile_inline: |
        FROM node:22-alpine
        WORKDIR /app
        RUN corepack enable
        COPY . /app
    command: sh -c "corepack prepare pnpm@latest --activate && pnpm install && pnpm dev --host 0.0.0.0 --port 3000"
    environment:
      - NODE_ENV=development
      - HOST=0.0.0.0
      - PORT=3000
# .spedy/preview.yml
service: app
port: 3000
views:
  - label: Storefront
    path: /

Shopware 6

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.yml
services:
  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
# .spedy/preview.yml
service: shopware
port: 80
views:
  - label: Storefront
    path: /
  - label: Admin
    path: /admin
hooks:
  post_agent:
    - run: bin/console theme:compile
      service: shopware
    - run: bin/console cache:clear
      service: shopware

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.

Felder in .spedy/preview.yml

FeldPflichtBedeutung
serviceempfohlenName des Compose-Service (Schlüssel unter services:), mit dem Traefik spricht. Weglassen → Auto-Detection (siehe unten).
portempfohlenContainer-interner Port, auf dem dein Service HTTP ausliefert (z. B. 3000, 8080, 80).
domainoptionalEine zusätzliche Host()-Regel neben <slug>.preview.<deine-domain> — z. B. eine feste Prod-Preview-Domain.
db.serviceoptionalDer Compose-Service, in den ein DB-Snapshot restauriert wird (gemountet in dessen /docker-entrypoint-initdb.d/). Nur nötig bei DB-Snapshots.
setupoptionalShell-Befehle, die nach compose up der Reihe nach im Primary-Container laufen (z. B. Migrationen, Seeds). Ein Nicht-Null-Schritt lässt den Boot fehlschlagen.
viewsoptionalNavigierbare Einstiegspunkte als Toggle in der Preview-UI (je label + path).
hooksoptionalIm 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_reloadoptionaltrue ü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.

Anforderungen an den Runner-Host

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.

  1. Docker-Socket. Der Supervisor steuert den Host-Daemon, um Preview-Stacks zu booten. Mounte ihn in den Supervisor-Service:
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
  2. 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:
    preview-traefik:
      image: traefik:v3.7
      command:
        - --providers.docker=true
        - --providers.docker.exposedbydefault=false
        - --providers.docker.network=spedy-preview
        - --entrypoints.web.address=:80
        - --entrypoints.websecure.address=:443   # muss existieren, sonst werden Pro-Preview-https-Router verworfen
      ports:
        - "127.0.0.1:9880:80"                     # der Supervisor proxyt Relay-Traffic hierher
      volumes:
        - /var/run/docker.sock:/var/run/docker.sock:ro
      networks:
        - spedy-preview
    networks:
      spedy-preview:
        name: spedy-preview
        external: true
    Ohne ihn läuft Preview-Traffic ins Leere (127.0.0.1:9880) und der Browser bekommt 404.
  3. 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.

Was Spedy mit deiner Compose macht

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.

Live-Änderungen (Agent-Edits → Preview)

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.

Ohne .spedy/preview.yml: Auto-Detection

Fehlt die Datei, versucht der Supervisor, den Primary-Service selbst zu finden:

  1. Ein Label gewinnt: der Service mit labels: { spedy.primary: "true" }.
  2. Sonst: der erste Service (alphabetisch) mit einem ports:- oder expose:-Eintrag.

Port-Auflösung (in dieser Reihenfolge): port aus preview.yml → erster ports:/expose:-Port des Service → Fallback 80.

Passt nichts, bricht der Start ab mit:

could not identify a primary HTTP service — declare it in .spedy/preview.yml (service + port), or add a ports:/expose: / spedy.primary label

Empfehlung: deklariere service + port explizit. Das ist eindeutig und übersteht Umbenennungen / zusätzliche Services.

DB-Snapshots, Setup & Agent-Hooks

.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.

setup:
  - php bin/console database:migrate --all
  - php bin/console theme:compile

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:

hooks:
  pre_agent:
    - composer install
  post_agent:
    - run: bin/console theme:compile
      service: shopware
      timeout: 600
    - bin/console cache:clear

Das vollständige .spedy/-Layout:

.spedy/
├── preview.yml       # Service/Port (+ domain, db, setup, views, hooks, hot_reload)
└── snapshot.sql.gz   # vom Supervisor beim Boot aus dem gewählten Plattform-
                      # Snapshot geschrieben — git-ignoriert, nie committet

Troubleshooting

SymptomUrsache / Fix
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 aufDie 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.ymlservice + port deklarieren.
Preview lädt, ist aber 502 / leerDein Service lauscht nicht auf dem in port angegebenen Container-Port, oder bindet 127.0.0.1 statt 0.0.0.0.
Preview-Host liefert 404Kein Preview-Ingress-Traefik im spedy-preview-Netz auf dem Runner-Host (Punkt 2 oben).
Der falsche Service wird geroutetAuto-Detection hat den falschen gewählt → service explizit setzen, oder labels: { spedy.primary: "true" } ergänzen.
docker-compose.spedy.yml taucht in git status aufSie sollte via .git/info/exclude ignoriert sein; wenn nicht, Preview einmal neu starten (der Supervisor ergänzt den Eintrag beim Boot).