SpedySpedy Docs

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:

  1. its own Compose file (which starts your stack), and
  2. a .spedy/preview.yml that 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 HTTP

That's it. When you click "Start preview" on a ticket, the runner:

  1. clones the repo (on the ticket's feature branch),
  2. reads .spedy/preview.yml,
  3. boots your docker-compose.yml and routes service/port under <orgSlug>--<ticket>.preview.spedy.test.

Fields in .spedy/preview.yml

FieldRequiredMeaning
servicerecommendedName of the compose service (the key under services:) Traefik talks to. Omit it → auto-detection (see below).
portrecommendedContainer-internal port your service serves HTTP on (e.g. 3000, 8080, 80).
domainoptionalAn 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: port is 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):

  1. A label wins: the service with labels: { spedy.primary: "true" }.
  2. Otherwise: the first service (alphabetically) with a ports: or expose: 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: 3000

Several services (app + DB)? Declare only the one that serves HTTP:

# docker-compose.yml has services: web, db, redis
# .spedy/preview.yml:
service: web
port: 8080

db/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) and container_name on 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 = your port),
  • mounts the cloned workspace at /opt/spedy-workspace in 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 here

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

Troubleshooting

SymptomCause / 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/emptyYour 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 routedAuto-detection picked the wrong one → set service explicitly, or add labels: { spedy.primary: "true" } to the right service.
Agent change not visibleNo 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 statusIt should be ignored via .git/info/exclude; if not, restart the preview once (the supervisor re-adds the entry on boot).