# Preview Deployments `apps/server/preview/` — Docker-based preview deployments for reviewing changes in a running application. ## Overview Preview deployments let reviewers spin up a branch in local Docker containers. A single shared **Caddy gateway** handles subdomain routing for all active previews, accessed at `http://.localhost:`. Two modes: - **Preview mode**: Checks out target branch into a git worktree, builds Docker images, serves production-like output. - **Dev mode**: Mounts the agent's worktree into a container with a dev server image (e.g. `node:20-alpine`), enabling hot reload. **Auto-start**: When a phase enters `pending_review`, a preview is automatically started for the phase's branch (if the initiative has exactly one project). **Key design decision: No database table.** Docker IS the source of truth. Instead of persisting rows, we query Docker directly via compose project names, container labels, and `docker compose` CLI commands. ## Setting Up Preview Deployments for a Project ### Prerequisites - **Docker** installed and running (`docker --version` / `docker compose version`) - **Project registered** in Codewalkers (`cw project add` or via the UI) - **Browser**: Chrome or Firefox recommended. Safari requires manual `/etc/hosts` entries for `*.localhost` subdomains. ### Quick Start 1. **Add a `.cw-preview.yml`** to your project root. This tells Codewalkers how to build and run your app: ```yaml version: 1 services: app: build: . port: 3000 ``` That's it for a single-service app with a Dockerfile. Run: ```sh cw preview start --initiative --project --branch ``` Open the URL printed in the output (e.g. `http://abc123.localhost:9100`). ### Don't Have a `.cw-preview.yml`? The config reader auto-discovers in this order: 1. **`.cw-preview.yml`** — full control (recommended) 2. **`docker-compose.yml` / `compose.yml`** — uses your existing compose file 3. **`Dockerfile`** — builds a single `app` service on port 3000 If your project already has a Dockerfile or compose file, previews work out of the box with zero config. ### Multi-Service Example A typical full-stack app with frontend, backend, and database: ```yaml version: 1 services: frontend: build: context: . dockerfile: apps/web/Dockerfile port: 3000 route: / # serves the root path healthcheck: path: / interval: 5s retries: 10 env: VITE_API_URL: /api dev: # optional: used in dev mode image: node:20-alpine command: npm run dev -- --host 0.0.0.0 workdir: /app backend: build: context: . dockerfile: packages/api/Dockerfile port: 8080 route: /api # proxied under /api/* healthcheck: path: /health env: DATABASE_URL: postgres://db:5432/app db: image: postgres:16-alpine port: 5432 internal: true # not exposed through the proxy env: POSTGRES_PASSWORD: preview ``` Requests to `http://.localhost:9100/` hit `frontend:3000`, requests to `/api/*` hit `backend:8080`, and `db` is only reachable by other services. ### Config Reference Each service in `.cw-preview.yml` supports: | Field | Required | Description | |-------|----------|-------------| | `build` | yes* | Build context — string (`"."`) or object (`{context, dockerfile}`) | | `image` | yes* | Docker image to pull (e.g. `postgres:16-alpine`) | | `port` | yes** | Container port the service listens on | | `route` | no | URL path prefix for gateway routing (default: `/`) | | `internal` | no | If `true`, not exposed through the proxy (e.g. databases) | | `healthcheck` | no | `{path, interval?, retries?}` — polled before marking ready | | `env` | no | Environment variables passed to the container | | `volumes` | no | Additional volume mounts | | `seed` | no | Array of shell commands to run inside the container after health checks pass | | `dev` | no | Dev mode overrides: `{image, command?, workdir?}` | \* Provide either `build` or `image`, not both. \** Required unless `internal: true`. ### Seeding If a service needs initialization (database migrations, fixture loading, etc.), add a `seed` array. Commands run inside the container via `docker compose exec` after all health checks pass, before the preview is marked ready. ```yaml services: app: build: . port: 3000 seed: - npm run db:migrate - npm run db:seed ``` Seeds execute in service definition order. Each command has a 5-minute timeout. If any seed command fails (non-zero exit), the preview fails and all containers are cleaned up. ### Dev Mode Dev mode skips the Docker build and instead mounts your source code into a container running a dev server. Useful for hot reload during active development. To use dev mode, add a `dev` section to the service: ```yaml services: app: build: . port: 3000 dev: image: node:20-alpine # base image with your runtime command: npm run dev -- --host 0.0.0.0 # dev server command workdir: /app # where source is mounted ``` Start with: ```sh cw preview start --initiative --project --branch --mode dev ``` The project directory is mounted at `workdir` (default `/app`). An anonymous volume is created for `node_modules` to prevent host/container conflicts. ### Healthchecks If a service has a `healthcheck`, the preview waits for it to respond with HTTP 200 before reporting ready. Without a healthcheck, the service is considered ready as soon as the container starts. ```yaml healthcheck: path: /health # required: endpoint to poll interval: 5s # optional: time between checks (default: 5s) retries: 10 # optional: max attempts before failing (default: 10) ``` ### Auto-Start on Review When a phase transitions to `pending_review`, a preview is automatically started if: - The initiative has exactly one registered project - The project has a discoverable config (`.cw-preview.yml`, compose file, or Dockerfile) No manual `cw preview start` needed — just push your branch and move the phase to review. ## Architecture ``` .cw-previews/ gateway/ docker-compose.yml ← single Caddy container, one port Caddyfile ← regenerated on each preview add/remove / docker-compose.yml ← per-preview stack (no published ports) source/ ← git worktree (preview mode only) Docker: network: cw-preview-net ← external, shared by gateway + all previews cw-preview-gateway ← Caddy on one port, subdomain routing cw-preview- ← per-preview compose project (services only) Routing: .localhost: → cw-preview--: ``` ``` PreviewManager ├── GatewayManager (shared Caddy gateway lifecycle + Caddyfile generation) ├── ConfigReader (discover .cw-preview.yml / compose / Dockerfile) ├── ComposeGenerator (generate per-preview docker-compose.yml) ├── DockerClient (thin wrapper around docker compose CLI + network ops) ├── HealthChecker (poll service healthcheck endpoints via subdomain URL) ├── PortAllocator (find next available port 9100-9200 for gateway) └── Worktree helper (git worktree add/remove for preview mode) ``` ### Lifecycle 1. **Start**: ensure gateway → discover config → create worktree (preview) or use provided path (dev) → generate compose → `docker compose up --build -d` → update gateway routes → health check → run seed commands → emit `preview:ready` 2. **Stop**: `docker compose down --volumes --remove-orphans` → remove worktree → clean up `.cw-previews//` → update gateway routes → stop gateway if no more previews → emit `preview:stopped` 3. **List**: `docker compose ls --filter name=cw-preview` → skip gateway project → parse container labels → reconstruct status 4. **Shutdown**: `stopAll()` called on server shutdown — stops all previews, then stops gateway ### Gateway The `GatewayManager` class manages a single shared Caddy container: - **`ensureGateway()`** — idempotent. Creates the `cw-preview-net` Docker network, checks if gateway is already running, allocates a port (9100-9200) if needed, writes compose + Caddyfile, starts Caddy with `--watch` flag. - **`updateRoutes()`** — regenerates the full Caddyfile from all active previews. Caddy's `--watch` flag auto-reloads on file change (no `docker exec` needed). - **`stopGateway()`** — composes down the gateway, removes the Docker network, cleans up the gateway directory. Gateway Caddyfile format: ``` { auto_https off } abc123.localhost:9100 { handle_path /api/* { reverse_proxy cw-preview-abc123-backend:8080 } handle { reverse_proxy cw-preview-abc123-frontend:3000 } } xyz789.localhost:9100 { handle { reverse_proxy cw-preview-xyz789-app:3000 } } ``` Routes are sorted by specificity (longer paths first) to ensure correct matching. ### Subdomain Routing Previews are accessed at `http://.localhost:`. - **Chrome / Firefox**: Resolve `*.localhost` to `127.0.0.1` natively. No DNS config needed. - **Safari**: Requires a `/etc/hosts` entry: `127.0.0.1 .localhost` for each preview. ### Docker Labels All preview containers get `cw.*` labels for metadata retrieval: | Label | Purpose | |-------|---------| | `cw.preview` | `"true"` — marker for filtering | | `cw.initiative-id` | Initiative ID | | `cw.phase-id` | Phase ID (optional) | | `cw.project-id` | Project ID | | `cw.branch` | Branch name | | `cw.port` | Gateway port | | `cw.preview-id` | Nanoid for this deployment | | `cw.mode` | `"preview"` or `"dev"` | | `cw.agent-id` | Agent ID (optional, set when started via `--agent`) | ### Compose Project Naming - **Gateway**: `cw-preview-gateway` (single instance) - **Previews**: `cw-preview-` — filtered via `docker compose ls --filter name=cw-preview`, gateway excluded from listings - **Container names**: `cw-preview--` — unique DNS names on the shared network ### Networking - **`cw-preview-net`** — external Docker bridge network shared by gateway + all preview stacks - **`internal`** — per-preview bridge network for inter-service communication - Public services join both networks; internal services (e.g. databases) only join `internal` ## Config Discovery See [Setting Up Preview Deployments](#setting-up-preview-deployments-for-a-project) above for the full config reference. Discovery order: 1. **`.cw-preview.yml`** — explicit CW preview config (recommended) 2. **`docker-compose.yml` / `compose.yml`** — existing compose file with gateway network injection 3. **`Dockerfile`** — single-service fallback (builds from `.`, assumes port 3000) ## Module Files | File | Purpose | |------|---------| | `types.ts` | PreviewConfig, PreviewStatus, labels, constants, dev config types | | `config-reader.ts` | Discovery + YAML parsing (including `dev` section) | | `compose-generator.ts` | Per-preview Docker Compose YAML + label generation | | `gateway.ts` | GatewayManager class + Caddyfile generation | | `worktree.ts` | Git worktree create/remove helpers | | `docker-client.ts` | Docker CLI wrapper (execa) + network operations | | `health-checker.ts` | Service readiness polling via subdomain URL | | `port-allocator.ts` | Port 9100-9200 allocation with TCP bind test | | `manager.ts` | PreviewManager class (start/stop/list/status/stopAll + auto-start) | | `index.ts` | Barrel exports | ## Events | Event | Payload | |-------|---------| | `preview:building` | `{previewId, initiativeId, branch, gatewayPort, mode, phaseId?}` | | `preview:ready` | `{previewId, initiativeId, branch, gatewayPort, url, mode, phaseId?}` | | `preview:stopped` | `{previewId, initiativeId}` | | `preview:failed` | `{previewId, initiativeId, error}` | ## Auto-Start `PreviewManager.setupEventListeners()` listens for `phase:pending_review` events: 1. Loads the initiative and its projects 2. If exactly one project: auto-starts a preview in `preview` mode 3. Branch is derived from `phaseBranchName(initiative.branch, phase.name)` 4. Errors are caught and logged (best-effort, never blocks the phase transition) ## Agent Integration Agents can spin up and tear down preview deployments using simplified commands that only require their agent ID — the server resolves initiative, project, branch, and mode automatically. ### Agent-Simplified Commands When an agent has a `` section in its prompt (injected automatically if the initiative's project has `.cw-preview.yml`), it can use: ``` cw preview start --agent # Server resolves everything, starts dev mode cw preview stop --agent # Stops all previews for this agent ``` ### Prompt Injection Preview instructions are automatically appended to agent prompts when all conditions are met: 1. Agent mode is `execute`, `refine`, or `discuss` 2. Agent has an `initiativeId` 3. Initiative has exactly one linked project 4. Project clone directory contains `.cw-preview.yml` The injected `` block includes prefilled `cw preview start/stop --agent ` commands. ### Agent ID Label Previews started with `--agent` receive a `cw.agent-id` Docker container label. This enables: - **Auto-teardown**: When an agent stops (`agent:stopped` event), all previews labeled with its ID are automatically torn down (best-effort). - **Agent-scoped stop**: `cw preview stop --agent ` finds and stops previews by label. ### Setup Command `cw preview setup` prints inline setup instructions for `.cw-preview.yml`. With `--auto --project `, it creates an initiative and spawns a refine agent to analyze the project and generate the config file. ## tRPC Procedures | Procedure | Type | Input | |-----------|------|-------| | `startPreview` | mutation | `{initiativeId, phaseId?, projectId, branch, mode?, worktreePath?, agentId?}` | | `startPreviewForAgent` | mutation | `{agentId}` | | `stopPreview` | mutation | `{previewId}` | | `stopPreviewByAgent` | mutation | `{agentId}` | | `listPreviews` | query | `{initiativeId?}` | | `getPreviewStatus` | query | `{previewId}` | `mode` defaults to `'preview'`. Set to `'dev'` with a `worktreePath` for dev mode. `startPreviewForAgent` always uses dev mode. ## CLI Commands ``` cw preview start --initiative --project --branch [--phase ] cw preview start --agent cw preview stop cw preview stop --agent cw preview list [--initiative ] cw preview status cw preview setup [--auto --project [--provider ]] ``` ## Frontend The Review tab shows preview status inline: - **No preview**: "Start Preview" button - **Building**: Spinner + "Building preview..." - **Running**: Green dot + `http://.localhost:` link + Stop button - **Failed**: Error message + Retry button Polls `getPreviewStatus` with `refetchInterval: 3000` while active. ## Container Wiring - `PreviewManager` instantiated in `apps/server/container.ts` with `(projectRepository, eventBus, workspaceRoot, phaseRepository, initiativeRepository)` - Added to `Container` interface and `toContextDeps()` - `GracefulShutdown` calls `previewManager.stopAll()` during shutdown - `requirePreviewManager(ctx)` helper in `apps/server/trpc/routers/_helpers.ts` ## Codewalkers Self-Preview Codewalkers itself has a Dockerfile, `.cw-preview.yml`, and seed script for preview deployments. This lets you demo the full app — initiatives, phases, tasks, agents with output, pages, and review tab with real git diffs. ### Files | File | Purpose | |------|---------| | `Dockerfile` | Multi-stage: deps → build (server+web) → production (Node + Caddy) | | `.cw-preview.yml` | Preview config: build, healthcheck, seed | | `scripts/Caddyfile` | Caddy config: SPA file server + tRPC reverse proxy | | `scripts/entrypoint.sh` | Init workspace, start backend, run Caddy | | `scripts/seed-preview.sh` | Create demo git repo + run Node.js seed | | `apps/server/preview-seed.ts` | Populate DB with demo initiative, phases, tasks, agents, log chunks, pages, review comments | ### Container Architecture Single container running two processes: - **Node.js backend** on port 3847 (tRPC API + health endpoint) - **Caddy** on port 3000 (SPA file server, reverse proxy `/trpc` and `/health` to backend) ### Seed Data The seed creates a "Task Manager Redesign" initiative with: - 3 phases (completed, pending_review, in_progress) - 9 tasks across phases - 3 agents with JSONL log output - Root page with Tiptap content - 3 review comments on the pending_review phase - Git repo with 3 branches and real commit diffs for the review tab ### Manual Testing ```sh docker build -t cw-preview . docker run -p 3000:3000 cw-preview # Wait for startup, then seed: docker exec sh /app/scripts/seed-preview.sh # Browse http://localhost:3000 ``` ## Dependencies - `js-yaml` + `@types/js-yaml` — for parsing `.cw-preview.yml` - `simple-git` — for git worktree operations - Docker must be installed and running on the host