Preview Config (BYOC)
Make any repository preview-ready with a .spedy/preview.yml file — declare the service and port Spedy routes to, or rely on auto-detection.
BYOC = Bring Your Own Compose. A preview boots your project's own
docker-compose.yml— there is no Shopware/Magento template involved. You only tell Spedy which service serves the preview.
For a repository to be usable as a per-ticket live preview, it needs two things in the repo:
- its own Compose file (which starts your stack), and
- a
.spedy/preview.ymlthat declares the service + port Spedy routes to the outside.
This page covers that config file. For the UI side — connecting the repo, uploading database snapshots, and using the preview inside a ticket — see Preview Environments.
Minimal setup
your-repo/
├── docker-compose.yml # your stack (BYOC)
├── .spedy/
│ └── preview.yml # which service/port is the preview?
└── … # your app code.spedy/preview.yml:
# Tells the Spedy supervisor how this preview is exposed.
service: app # name of the compose service Traefik routes to
port: 3000 # container port that serves HTTPThat's it. When you click "Start preview" on a ticket, the runner:
- clones the repo (on the ticket's feature branch),
- reads
.spedy/preview.yml, - boots your
docker-compose.ymland routesservice/portunder<orgSlug>--<ticket>.preview.spedy.test.
Fields in .spedy/preview.yml
| Field | Required | Meaning |
|---|---|---|
service | recommended | Name of the compose service (the key under services:) Traefik talks to. Omit it → auto-detection (see below). |
port | recommended | Container-internal port your service serves HTTP on (e.g. 3000, 8080, 80). |
domain | optional | An extra Host() rule alongside <slug>.preview.spedy.test — e.g. a fixed prod-preview domain. |
.spedy/preview.yaml (with an a) is accepted too.
Important:
portis the port inside the container, not a host mapping. Your service must listen on that port inside the container. Host port mappings (ports:) in your Compose are stripped by Spedy (several parallel previews would otherwise collide) — you don't need them for the preview.
Without .spedy/preview.yml: auto-detection
If the file is missing, the supervisor tries to find the primary service itself (pickPrimary):
- A label wins: the service with
labels: { spedy.primary: "true" }. - Otherwise: the first service (alphabetically) with a
ports:orexpose:entry.
Port resolution (in this order): port from preview.yml → the service's first ports:/expose: port → fallback 80.
If it finds no service with a port and you declared nothing, the start aborts with:
could not identify a primary HTTP service — declare it in .spedy/preview.yml (service + port), or add a ports:/expose: / spedy.primary label
Recommendation: don't rely on the heuristic — declare service + port explicitly in .spedy/preview.yml. It's unambiguous and survives renames / additional services.
Full example (Node app)
docker-compose.yml:
services:
app:
build: .
ports:
- "3000:3000" # host mapping for local dev only — Spedy strips it
restart: unless-stopped.spedy/preview.yml:
service: app
port: 3000Several services (app + DB)? Declare only the one that serves HTTP:
# docker-compose.yml has services: web, db, redis
# .spedy/preview.yml:
service: web
port: 8080db/redis still run, but aren't routed to the outside.
What Spedy does with your Compose
On boot the supervisor renders a derived docker-compose.spedy.yml next to your original (so build: . stays relative to the repo). In doing so it:
- strips
ports:(host mappings) andcontainer_nameon all services (collisions across parallel previews), - attaches the primary service to the external Docker network
spedy-preview, - injects Traefik labels (routing to
<slug>.preview.spedy.test,loadbalancer.server.port= yourport), - mounts the cloned workspace at
/opt/spedy-workspacein the primary container (where the coding agent works, see below).
The generated docker-compose.spedy.yml is ignored locally via .git/info/exclude — it never lands in your repo/PR.
Hot reload (so agent changes show up live)
The coding agent edits the workspace at /opt/spedy-workspace. For its changes to appear in the preview immediately, your preview stack should run in dev mode with watch from the mounted source, e.g.:
services:
app:
build: .
working_dir: /opt/spedy-workspace
command: npm run dev # next dev / node --watch / vite …
# the port is set in .spedy/preview.yml, not hereWithout hot reload, Spedy rebuilds the preview after each agent run (--build) so the change takes effect — that works too, but takes longer.
Optional: snapshots & branch hooks
.spedy/ can hold more than preview.yml — database snapshots and per-branch lifecycle hooks. This is independent of BYOC. The snapshot resolution order is covered under Preview Environments → Override the database per ticket.
.spedy/
├── preview.yml # this file (BYOC service/port)
├── snapshots/
│ ├── default.sql.gz # DB default
│ └── <branch>.sql.gz # branch-specific DB state
└── hooks/
├── post-clone.sh
└── post-checkout.shTroubleshooting
| Symptom | Cause / fix |
|---|---|
| Start aborts: "could not identify a primary HTTP service" | No service with a port + no .spedy/preview.yml → declare service + port. |
| Preview loads but is 502/empty | Your service isn't listening on the container port given in port. Check what the app binds to inside the container (0.0.0.0:<port>, not 127.0.0.1). |
| The wrong service is routed | Auto-detection picked the wrong one → set service explicitly, or add labels: { spedy.primary: "true" } to the right service. |
| Agent change not visible | No hot reload in the stack → see "Hot reload", or the preview is rebuilt after the run (give it a moment). |
docker-compose.spedy.yml shows up in git status | It should be ignored via .git/info/exclude; if not, restart the preview once (the supervisor re-adds the entry on boot). |