# Health check Source: https://docs.prvue.dev/api-reference/health/health-check openapi.json get /health Returns service health status and uptime. # Delete a preview deployment Source: https://docs.prvue.dev/api-reference/previews/delete-a-preview-deployment openapi.json delete /api/previews/{deploymentId} Stops and removes the preview for the given deployment id (format: {projectSlug}-{prNumber}, e.g. myorg-myapp-12). # List preview deployments Source: https://docs.prvue.dev/api-reference/previews/list-preview-deployments openapi.json get /api/previews Returns all tracked preview deployments. # GitHub webhook Source: https://docs.prvue.dev/api-reference/webhook/github-webhook openapi.json post /webhook/github Receives GitHub pull_request webhook events (opened, reopened, synchronize, closed). Requires X-Hub-Signature-256 header. Payload follows GitHub webhook payload format. # Architecture Source: https://docs.prvue.dev/architecture System design, components, data flow, and security for Prvue Prvue is a multi-component system that automatically creates isolated preview environments for backend applications when GitHub PRs are opened. ## Architecture Diagram ```mermaid theme={null} graph TB GitHub[GitHub Repository] -->|Webhook| Orchestrator[Orchestrator API] Orchestrator -->|Clone & Build| Docker[Docker Containers] Orchestrator -->|Configure| Nginx[Nginx Reverse Proxy] Docker -->|App + DB| Nginx Nginx -->|Route| Preview[Preview URLs] Orchestrator -->|Comments| GitHub Cleanup[Cleanup Service] -->|TTL Check| Orchestrator Cleanup -->|Cleanup| Docker Cleanup -->|Remove Config| Nginx ``` ## Components ### 1. Terraform **Purpose**: Infrastructure as Code for Digital Ocean provisioning **Responsibilities**: * Create Digital Ocean droplet * Configure firewall rules * Allocate reserved IP * Set up SSH access **Key Files**: * `terraform/main.tf`: Resource definitions * `terraform/variables.tf`: Input variables * `terraform/outputs.tf`: Output values ### 2. Ansible **Purpose**: Server configuration and service deployment **Responsibilities**: * Install Docker and Docker Compose * Install and configure nginx * Deploy orchestrator service * Set up systemd services * Configure log rotation **Roles**: * **docker**: Docker installation and configuration * **nginx**: Nginx installation and preview config structure * **orchestrator**: Orchestrator service deployment ### 3. Orchestrator Service **Purpose**: Core service handling webhooks and managing deployments **Responsibilities**: * Receive and verify GitHub webhooks * Clone repositories * Detect framework (NestJS, Go, Laravel, Rust, Python) * Generate Docker Compose files * Build and start containers * Configure nginx routing * Post comments to GitHub PRs * Cleanup stale deployments **Key Modules**: * **webhook-handler.ts**: Webhook processing and routing * **docker-manager.ts**: Docker operations * **nginx-manager.ts**: Nginx configuration management * **github-client.ts**: GitHub API interactions * **cleanup-service.ts**: Scheduled cleanup tasks * **deployment-tracker.ts**: Deployment state management ### 4. CLI Tool **Purpose**: User-facing interface for setup and management **Responsibilities**: * Initialize configuration * Run Terraform and Ansible * Create GitHub webhooks * Sync orchestrator code to server (after setup) * Check deployment status * Destroy infrastructure **Commands**: * `init`: Create configuration file * `setup`: Deploy infrastructure * `sync`: Sync orchestrator code to server (build locally, rsync, restart). Use after setup when you change orchestrator code. * `status`: Check system status * `destroy`: Teardown infrastructure ## Data Flow ### Deployment Flow ``` 1. PR Opened → GitHub Webhook → Orchestrator 2. Orchestrator validates repository and webhook signature 3. Orchestrator clones repository and checks out PR branch 4. Framework detection (NestJS, Go, Laravel, Rust, Python) 5. Generate Docker Compose file from template 6. Allocate ports (app: 8000 + PR#, db: 9000 + PR#) 7. Build and start containers 8. Wait for health check 9. Generate nginx config 10. Reload nginx 11. Post comment to PR with preview URL 12. Save deployment info to tracking store ``` ### Update Flow ``` 1. PR Updated → GitHub Webhook → Orchestrator 2. Pull latest changes from repository 3. Rebuild containers 4. Wait for health check 5. Update PR comment with new preview URL ``` ### Cleanup Flow ``` 1. PR Closed OR TTL Expired → Cleanup Service 2. Stop Docker containers 3. Remove Docker volumes 4. Delete nginx config 5. Reload nginx 6. Remove deployment from tracking store 7. Release ports ``` ## Port Allocation Strategy * **Global pool**: App ports start at 8000, DB ports at 9000. Each new deployment gets the next free app port and next free db port (keyed by deployment id). * Port allocations are tracked in the deployment store's `portAllocations` map (keyed by deployment id) and are released on cleanup so ports are reused correctly. Allocation excludes host ports currently in use by running Docker containers, so failed deployments whose containers still run do not cause port collisions. * Allows many deployments across multiple repos without collision. ## Routing Strategy **Path-based routing**: `/{PROJECT_SLUG}/pr-{PR_NUMBER}/` * **Project slug**: From repo owner/name (e.g. `myorg-myapp`). Avoids collisions when multiple repos have the same PR number. * Example: `http://SERVER_IP/myorg-myapp/pr-12/` * nginx proxies to `http://localhost:{APP_PORT}/` * Path prefix is stripped in proxy configuration **Future**: Subdomain support (`pr-123.server.com`) requires DNS configuration. ## Security ### Webhook Security * **Signature Verification**: HMAC SHA256 verification of webhook payloads * **Repository Whitelist**: Only allowed repositories can trigger deployments * **Input Sanitization**: PR numbers and branch names are validated ### Container Security * **Resource Limits**: CPU and memory limits per container * **Non-root Users**: Containers run as non-root users * **Network Isolation**: Containers are isolated on Docker network * **Health Checks**: Containers must pass health checks before being routed ### Infrastructure Security * **SSH Key Authentication**: Only SSH key access to droplet * **Firewall Rules**: Only necessary ports open (22, 80, 443) * **Internal API**: Orchestrator API not exposed publicly (internal port 3000) * **Keychain Storage**: Sensitive tokens stored in OS keychain ## Deployment Tracking Deployments are tracked in a JSON file (`/opt/preview-deployer/deployments.json`). Keys are **deployment ids** (`{projectSlug}-{prNumber}`): ```json theme={null} { "deployments": { "my-org-my-app-12": { "prNumber": 12, "repoName": "my-app", "repoOwner": "my-org", "projectSlug": "my-org-my-app", "deploymentId": "my-org-my-app-12", "branch": "feature-branch", "commitSha": "abc123...", "framework": "nestjs", "dbType": "postgres", "appPort": 3000, "exposedAppPort": 8000, "exposedDbPort": 9000, "status": "running", "createdAt": "2026-01-29T10:00:00Z", "updatedAt": "2026-01-29T10:00:00Z", "url": "http://SERVER_IP/my-org-my-app/pr-12/", "commentId": 456789 } }, "portAllocations": { "my-org-my-app-12": { "exposedAppPort": 8000, "exposedDbPort": 9000 } } } ``` ## Scaling Considerations ### Current Limitations * **Single Server**: All previews run on one droplet * **Resource Limits**: Limited by droplet size * **Port Range**: Maximum \~56k PRs per repo ### Future Scaling Options 1. **Horizontal Scaling**: Multiple droplets with load balancer 2. **Kubernetes**: Container orchestration for better resource management 3. **Subdomain Routing**: DNS-based routing instead of path-based 4. **Database Pooling**: Shared database instances for cost savings 5. **Caching**: Docker image caching for faster builds ## Monitoring ### Current Monitoring * **Logs**: Orchestrator logs to `/opt/preview-deployer/logs/` * **Systemd**: Service status via `systemctl status` * **Health Endpoint**: `/health` endpoint for basic health checks ### Future Monitoring * **Metrics**: Prometheus metrics export * **Alerting**: Alertmanager integration * **Dashboard**: Grafana dashboard for visualization * **Tracing**: Distributed tracing for request flows ## Error Handling ### Retry Logic * **Transient Failures**: Max 3 retries with exponential backoff * **Health Checks**: 60-second timeout with 5-second intervals * **GitHub API**: Automatic retry on rate limits ### Error Reporting * **GitHub Comments**: Failure comments posted to PRs * **Logging**: Comprehensive error logging with context * **Status Tracking**: Deployment status tracked (building/running/failed) ## Cost Management ### Resource Limits * **Default TTL**: 7 days * **Max Concurrent**: 10 previews (configurable) * **Container Limits**: 512MB RAM, 0.5 CPU per container ### Cost Optimization * **Auto Cleanup**: Stale previews cleaned automatically * **Resource Limits**: Prevent resource exhaustion * **Efficient Builds**: Docker layer caching * **Small Droplets**: Use smallest droplet size that fits needs # Configuration Reference Source: https://docs.prvue.dev/configuration Complete reference for all configuration options in Prvue Complete reference for all configuration options in Prvue. ## Repository Configuration (`preview-config.yml`) **Required.** Place this file in your repository root. The orchestrator reads it after cloning; if the file is missing or validation fails, the deployment fails with a clear error. Required fields are validated; optional fields override defaults and auto-detection (e.g. framework is taken from this file when present, otherwise detected from the repo). ### Required Fields ```yaml theme={null} # Framework selection (overrides auto-detection when set) framework: nestjs # Options: nestjs, go, laravel, rust, python # Database type database: postgres # Options: postgres, mysql, mongodb # Health check endpoint path (used when waiting for the app to be ready). Must start with / health_check_path: /health # Port the app listens on inside the container app_port: 3000 # Environment variable name for the app port (e.g. PORT) app_port_env: PORT # Entrypoint for the app; used in the Dockerfile CMD. Examples by framework: # NestJS: dist/main.js Go: server Rust: ./app or ./target/release/app Python: app.main:app # Laravel: value is required but unused (php artisan serve is always used) app_entrypoint: dist/main.js ``` ### Optional Fields ```yaml theme={null} # Commands run on the host in repo root before docker compose up (e.g. copy .env). # Non-zero exit fails the deployment. Use for file setup, not for npm install (that stays in Dockerfile). build_commands: - cp .env.example .env - mkdir -p uploads # Extra infra: list of known template names. App is wired automatically (e.g. REDIS_URL for redis). extra_services: - redis # Adds Redis service; app gets REDIS_URL=redis://redis:6379 (BullMQ, cache, etc.) # Environment variables (injected at runtime into the app container; keep .env in .dockerignore) env: - NODE_ENV=preview - DEBUG=true - API_KEY=value # Optional: Env file path (single string) relative to repo root. Compose loads this file into the app container at runtime. # Use with build_commands: [cp .env.example .env] so the file exists before docker compose up. # env_file: .env # Commands run inside the app container before the main process (migrations, seeding, etc.). # Runs in order; non-zero exit fails the container. Then the app starts as usual. startup_commands: - npm run migration:run - npm run seed # Or: npx prisma migrate deploy && npx prisma db seed # Custom Dockerfile path (relative to repo root) dockerfile: ./Dockerfile ``` ### Repo-owned preview Compose (`docker-compose.preview.yml`) You can provide your own Docker Compose file **strictly for preview** by placing `docker-compose.preview.yml` or `docker-compose.preview.yaml` in your repository root (exact names only; no fuzzy matching). When present, the orchestrator uses it instead of generating one from framework templates (same idea as using your own Dockerfile). * **File names**: `docker-compose.preview.yml` or `docker-compose.preview.yaml` (in repo root). Only these two exact filenames are accepted. If you use `.yaml`, the orchestrator renames it to `.yml` so one standard path is used everywhere. * **When used**: If either file exists after clone/checkout, the orchestrator parses it, injects host port mappings (see Ports), and writes `docker-compose.preview.generated.yml` in the deployment directory. That generated file is used for `docker compose up/down`. Otherwise the orchestrator generates `docker-compose.preview.yml` from templates. * **Ports**: Do **not** specify host ports for the `app` or `db` services in your compose file. The orchestrator injects them at runtime so each preview gets unique ports and nginx can route correctly. Use service names `app` and `db`. Container ports are inferred from framework (NestJS 3000, Go 8080, Laravel 8000, Rust 8080, Python 8000) and database type (Postgres 5432, MySQL 3306, MongoDB 27017). If you omit `ports` for `app`/`db`, we add them; if you had host ports, we override them. * **Project name**: The orchestrator always runs with `-p ` (e.g. `myorg-myapp-12`). Do not rely on a fixed project name in your file. * **Rebuild/cleanup**: Update and cleanup use the same generated file: `docker-compose.preview.generated.yml` when you provide repo compose, otherwise `docker-compose.preview.yml` (orchestrator-generated). ## CLI Configuration (`~/.preview-deployer/config.yml`) Generated by `preview init`. Sensitive values are stored in OS keychain. ```yaml theme={null} digitalocean: token: keychain # Stored in OS keychain region: nyc3 droplet_size: s-2vcpu-4gb github: token: keychain # Stored in OS keychain webhook_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx repositories: - owner/repo-name orchestrator: cleanup_ttl_days: 7 max_concurrent_previews: 10 ``` ## Environment Variables ### Orchestrator Service Set in Ansible or systemd service file. When running locally (e.g. for development), the orchestrator loads a `.env` file from its working directory if present, so you can keep secrets out of the shell. ```bash theme={null} # GitHub Configuration GITHUB_TOKEN=ghp_xxxxxxxxxxxx GITHUB_WEBHOOK_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ALLOWED_REPOS=owner/repo1,owner/repo2 # Preview Configuration PREVIEW_BASE_URL=http://YOUR_SERVER_IP CLEANUP_TTL_DAYS=7 MAX_CONCURRENT_PREVIEWS=10 # Server Configuration ORCHESTRATOR_PORT=3000 NODE_ENV=production # Deployment Paths DEPLOYMENTS_DIR=/opt/preview-deployments NGINX_CONFIG_DIR=/etc/nginx/preview-configs DEPLOYMENTS_DB=/opt/preview-deployer/deployments.json # Logging (orchestrator uses pino) LOG_LEVEL=info # Options: debug, info, warn, error # Optional: Base URL for OpenAPI spec servers[].url. Defaults to PREVIEW_BASE_URL when unset. # Set to the public URL of the orchestrator (e.g. https://preview.example.com) when your doc site or Swagger UI should reference it. # ORCHESTRATOR_PUBLIC_URL=https://preview.example.com ``` ### API reference (OpenAPI) The orchestrator exposes OpenAPI 3.0 documentation so you can generate API references (e.g. for a separate doc site) or explore the API interactively. * **OpenAPI JSON**: `GET /openapi.json` — Returns the OpenAPI 3.0 spec. Your doc site or API tooling can fetch this URL (from the orchestrator host, e.g. `http://SERVER_IP:3000/openapi.json`) to generate the API reference. * **Swagger UI**: `GET /api-docs` — Serves Swagger UI that loads the same spec, for interactive exploration (similar to NestJS's default docs page). The spec's `servers[].url` is set from `ORCHESTRATOR_PUBLIC_URL` if present, otherwise from `PREVIEW_BASE_URL`. If neither is set, the spec omits `servers` and clients can use the request host as the base URL. ## Terraform Variables Set in `terraform/terraform.tfvars`: ```hcl theme={null} do_token = "your-digital-ocean-api-token" ssh_public_key = "ssh-rsa AAAA..." region = "nyc3" # Options: nyc1, nyc3, sfo3, ams3, etc. droplet_size = "s-2vcpu-4gb" # Options: s-1vcpu-2gb, s-2vcpu-4gb, s-4vcpu-8gb project_name = "preview-deployer" ``` ### Available Regions * `nyc1`, `nyc3`: New York * `sfo3`: San Francisco * `ams3`: Amsterdam * `sgp1`: Singapore * `lon1`: London * `fra1`: Frankfurt * `tor1`: Toronto * `blr1`: Bangalore ### Available Droplet Sizes * `s-1vcpu-2gb`: \$12/month * `s-2vcpu-4gb`: \$24/month (default) * `s-4vcpu-8gb`: \$48/month * `s-8vcpu-16gb`: \$96/month ## Ansible Variables Set via `-e` flag or in playbook: ```yaml theme={null} deployment_user: preview-deployer orchestrator_dir: /opt/preview-deployer orchestrator_port: 3000 github_token: ghp_xxxxxxxxxxxx github_webhook_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx allowed_repos: owner/repo1,owner/repo2 server_ip: 1.2.3.4 preview_base_url: http://1.2.3.4 cleanup_ttl_days: 7 max_concurrent_previews: 10 orchestrator_log_level: info # optional; options: debug, info, warn, error ``` ### Optional SSL (Let's Encrypt) When you have a domain pointing at the server, set these so the nginx role obtains a certificate and serves HTTPS: ```yaml theme={null} preview_domain: preview.example.com # FQDN that resolves to the droplet ssl_email: admin@example.com # Used for Let's Encrypt agree-tos ``` Then set `preview_base_url` (and orchestrator env) to `https://preview.example.com` so PR comments get HTTPS preview links. Certbot runs via the nginx role (webroot); HTTP is redirected to HTTPS and ACME challenges are served on port 80 for renewal. ## Docker Compose Templates Templates are located under `orchestrator/templates/docker-compose/` and `orchestrator/templates/dockerfile/`: * `docker-compose/docker-compose.nestjs.yml.hbs`: NestJS application * `docker-compose/docker-compose.go.yml.hbs`: Go application * `docker-compose/docker-compose.laravel.yml.hbs`: Laravel application * `docker-compose/docker-compose.rust.yml.hbs`: Rust application * `docker-compose/docker-compose.python.yml.hbs`: Python application * `dockerfile/Dockerfile.nestjs.hbs`, `Dockerfile.go.hbs`, `Dockerfile.laravel.hbs`, `Dockerfile.rust.hbs`, `Dockerfile.python.hbs`: Default Dockerfiles per framework ### Template Variables * `{{prNumber}}`: PR number * `{{appPort}}`: Allocated app port (exposed on host) * `{{dbPort}}`: Allocated database port (exposed on host) ### Container Resources Default limits (configurable in templates): ```yaml theme={null} deploy: resources: limits: cpus: '0.5' memory: 512M reservations: cpus: '0.25' memory: 256M ``` ## Nginx Configuration The nginx role installs nginx and a default server block (port 80, or 80+443 when SSL is enabled). Optional SSL is handled inside the same role: when `preview_domain` and `ssl_email` are set, it installs certbot, obtains a certificate (webroot), and re-deploys nginx with listen 443 and HTTP→HTTPS redirect. Preview configs are generated in `/etc/nginx/preview-configs/`. That directory is owned by the deployment user (e.g. `preview-deployer`) so the orchestrator can create and remove config files without root. After writing a config, the orchestrator runs `nginx -t` and `nginx -s reload` via sudo; Ansible deploys a sudoers fragment at `/etc/sudoers.d/preview-deployer-nginx` so the deployment user can run only those two nginx commands without a password. The orchestrator systemd unit has `NoNewPrivileges=false` so that sudo can be used for this limited reload, and `ReadWritePaths` includes `/var/log/nginx` and `/run` so the nginx child process can write its error log and pid file when testing/reloading. ```nginx theme={null} location /{PROJECT_SLUG}/pr-{PR_NUMBER}/ { proxy_pass http://localhost:{APP_PORT}/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; proxy_read_timeout 300s; proxy_connect_timeout 75s; } ``` ## Health Check Configuration Your application must expose a health check endpoint: ### NestJS Example ```typescript theme={null} @Controller() export class AppController { @Get('health') health() { return { status: 'ok' }; } } ``` ### Go Example ```go theme={null} func healthHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"ok"}`)) } ``` The orchestrator will poll this endpoint until it returns 200 OK. ## Framework detection and Dockerfiles The orchestrator supports five frameworks: NestJS, Go, Laravel, Rust, and Python. It detects framework from the cloned repo (when not set in `preview-config.yml`) and uses the matching docker-compose template from `orchestrator/templates/docker-compose/`. **If the repo has no Dockerfile**, the orchestrator injects a default one for that framework from `orchestrator/templates/dockerfile/`. Repos can override by providing their own `Dockerfile` at the repo root. Detection order: NestJS (nest-cli.json or `@nestjs/core` in package.json) → Go (go.mod) → Laravel (`laravel/framework` in composer.json). Rust and Python are not auto-detected; set `framework: rust` or `framework: python` in `preview-config.yml`. If none match, NestJS is assumed. ## Custom Dockerfiles If your repository has a custom Dockerfile, ensure it: 1. Exposes the correct port (3000 NestJS, 8080 Go, 8000 Laravel) 2. Includes a health check endpoint (or `/health` for orchestrator polling) 3. Runs as non-root user (recommended) 4. Handles SIGTERM gracefully ### Example Dockerfile (NestJS) ```dockerfile theme={null} FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --production COPY . . RUN npm run build EXPOSE 3000 USER node CMD ["node", "dist/main.js"] ``` ## Database Configuration ### PostgreSQL (Default) ```yaml theme={null} database: postgres ``` Connection string format: ``` postgresql://preview:preview@db:5432/pr_{PR_NUMBER} ``` ### MySQL ```yaml theme={null} database: mysql ``` Connection string format: ``` mysql://preview:preview@db:3306/pr_{PR_NUMBER} ``` ### MongoDB ```yaml theme={null} database: mongodb ``` Connection string format: ``` mongodb://preview:preview@db:27017/pr_{PR_NUMBER} ``` ## GitHub Webhook Configuration Webhooks are automatically created by the CLI. Manual configuration: 1. Go to repository Settings > Webhooks 2. Add webhook: * **Payload URL**: `http://YOUR_SERVER_IP/webhook/github` * **Content type**: `application/json` * **Secret**: From `~/.preview-deployer/config.yml` * **Events**: Select "Pull requests" * **Active**: Checked ## Logging Configuration ### Orchestrator Logs The orchestrator uses **pino** and writes to stdout/stderr. In production, the systemd unit redirects stdout to `.../logs/orchestrator.log` and stderr to `.../logs/orchestrator-error.log`. Location: `/opt/preview-deployer/logs/` * `orchestrator.log`: Application logs (stdout) * `orchestrator-error.log`: Error logs (stderr) View logs (primary: tail the log files): ```bash theme={null} ssh root@YOUR_SERVER_IP tail -f /opt/preview-deployer/logs/orchestrator.log # Errors: tail -f /opt/preview-deployer/logs/orchestrator-error.log ``` Alternatively, use `journalctl -u preview-orchestrator -f` for the service unit output. ### Docker Logs View container logs: ```bash theme={null} docker logs {projectSlug}-pr-{PR_NUMBER}-app docker logs {projectSlug}-pr-{PR_NUMBER}-db ``` ### Nginx Logs Location: `/var/log/nginx/` * `access.log`: Access logs * `error.log`: Error logs ## Troubleshooting Configuration See [Troubleshooting Guide](/troubleshooting) for common configuration issues. # Go Example Source: https://docs.prvue.dev/examples/go Example Go repo with PostgreSQL and Redis for preview-deployer This example uses the **supported** Go framework. The orchestrator has a built-in template; you add `preview-config.yml` and the app is built and run in a container. ## Repository * **Repo**: [go-project-preview-example](https://github.com/your-org/go-project-preview-example) — replace with your repo URL when published. ## Stack * **Framework**: Go (Gin) * **Database**: PostgreSQL * **Cache**: Redis * **Config**: `.env` (e.g. `DATABASE_URL`, `REDIS_URL`) * **Health**: `GET /health` (liveness), `GET /ready` (readiness with Postgres + Redis) * **Endpoints**: `GET /api/admins`, `GET /api/stats` * **Migrations**: Applied on startup; SQL in `migrations/001_init.sql` ## Key files ### `preview-config.yml` (repository root) ```yaml theme={null} framework: go database: postgres health_check_path: /health app_port: 8080 app_port_env: PORT app_entrypoint: ./server extra_services: - redis ``` The orchestrator generates `docker-compose.preview.yml` from the Go template. The app container typically listens on port **8080**; the orchestrator maps it to an allocated host port and polls `health_check_path` until 200 OK. ### Optional: `docker-compose.preview.yml` If you need a custom compose file (e.g. extra services), add `docker-compose.preview.yml` or `docker-compose.preview.yaml` in the repo root. Do not set host ports for `app` or `db`—the orchestrator injects them. ## Local run and API See the repo README for: * `cp .env.example .env`, `docker compose up -d` for Postgres/Redis * `go run ./cmd/server` * Default port 8080; migrations and admin seed on startup Endpoints: `/health`, `/ready`, `/api/admins`, `/api/stats`. # Examples Overview Source: https://docs.prvue.dev/examples/index Example repositories showing how to add preview-config and optional docker-compose.preview for Prvue Prvue creates per-PR environments for your backend. These example repositories show how to add `preview-config.yml`, optional `docker-compose.preview.yml` (or `.yaml`), and health checks so your app works with the orchestrator. ## Supported frameworks | Type | Frameworks | How it works | | ------------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Supported** | NestJS, Laravel, Go, Rust, Python | Orchestrator has built-in Docker Compose and Dockerfile templates for all five. You add `preview-config.yml`; the orchestrator generates `docker-compose.preview.yml` and optionally injects a default Dockerfile if your repo has none. | | **Custom compose** | Any other stack | No built-in template. You add a **repo-owned** `docker-compose.preview.yml` (or `.yaml`) in the repo root. The orchestrator uses it and injects host ports for app/db. You still add `preview-config.yml` with required fields (e.g. `framework`, `health_check_path`, `app_port`, `app_entrypoint`). | ## Example repositories PostgreSQL, Redis, health/stats/admins. **Supported** framework (built-in template). Laravel, PostgreSQL, Redis, `.env`. **Supported** framework (built-in template). Go, PostgreSQL, Redis. **Supported** framework (built-in template). FastAPI, Postgres, Redis. **Supported** framework (built-in template); can use repo-owned compose for custom needs. Rust API, Postgres, Redis. **Supported** framework (built-in template); can use repo-owned compose for custom needs. ## What to add in your repo 1. **`preview-config.yml`** (repository root) * `framework`: `nestjs` | `go` | `laravel` | `rust` | `python`. * `database`: `postgres` | `mysql` | `mongodb`. * `health_check_path`: path the orchestrator will poll (e.g. `/health`). * Required: `app_port`, `app_port_env`, `app_entrypoint`. Optional: `build_commands`, `startup_commands`, `extra_services` (e.g. `redis`), `env`, `dockerfile`. 2. **`docker-compose.preview.yml` or `docker-compose.preview.yaml`** (optional for all supported frameworks) * For NestJS, Laravel, Go, Rust, or Python you can omit it and use the generated compose from the built-in template. * To customize services or use another stack, put a repo-owned compose file with `app` and `db` services; do not set host ports—the orchestrator injects them. 3. **Health endpoint** * Your app must expose the path you set in `health_check_path` and return 200 when ready. If example repos are private or not yet published, replace repo URLs in the subpages with your own; the structure and key files are the same. # Laravel Example Source: https://docs.prvue.dev/examples/laravel Example Laravel repo with PostgreSQL, Redis, and .env for preview-deployer This example uses the **supported** Laravel framework. The orchestrator has a built-in template; you add `preview-config.yml` and optionally build/startup commands for migrations and seeding. ## Repository * **Repo**: [laravel-project-preview-example](https://github.com/your-org/laravel-project-preview-example) — replace with your repo URL when published. ## Stack * **Framework**: Laravel (PHP) * **Database**: PostgreSQL * **Cache**: Redis * **Config**: `.env` (e.g. `DB_*`, `REDIS_*`) * **Health**: `GET /up` (Laravel built-in), `GET /api/health` (JSON with DB + Redis checks) * **Endpoints**: `GET /api/version`, `GET /api/users/count` ## Key files ### `preview-config.yml` (repository root) ```yaml theme={null} framework: laravel database: postgres health_check_path: /up # Or use /api/health if you prefer the JSON health endpoint # health_check_path: /api/health app_port: 8000 app_port_env: PORT app_entrypoint: php artisan serve # (app_entrypoint is required but Laravel always uses php artisan serve) build_commands: - cp .env.example .env - php artisan key:generate startup_commands: - php artisan migrate --force - php artisan db:seed --force extra_services: - redis ``` The orchestrator generates `docker-compose.preview.yml` from the Laravel template. Ensure `.env` is created before `docker compose up` (e.g. via `build_commands`); set `DB_*` and `REDIS_*` to match the compose services (e.g. `db`, `redis`). ### Optional: `docker-compose.preview.yaml` If the repo already has a `docker-compose.preview.yml` or `docker-compose.preview.yaml`, the orchestrator uses it and injects host ports for `app` and `db`. Otherwise the built-in Laravel template is used. ## Local run and API See the repo README for: * Copy `.env.example` to `.env`, set `DB_*` and `REDIS_*`, run `php artisan key:generate` * Migrations and seed: `php artisan migrate --force && php artisan db:seed --force` * `php artisan serve` or Docker Compose API: `GET /up`, `GET /api/health`, `GET /api/version`, `GET /api/users/count`. # NestJS Example Source: https://docs.prvue.dev/examples/nestjs Example NestJS repo with PostgreSQL, Redis, health endpoints, and admin seeder for preview-deployer This example uses the **supported** NestJS framework. The orchestrator has a built-in template; you add `preview-config.yml` and optionally a custom Dockerfile. ## Repository * **Repo**: [nestjs-project-preview-example](https://github.com/your-org/nestjs-project-preview-example) — replace with your repo URL when published. ## Stack * **Framework**: NestJS (Node.js) * **Database**: PostgreSQL * **Cache**: Redis * **Config**: Env-based (e.g. `DATABASE_URL`, `REDIS_URL`) * **Health**: `GET /health` (DB + Redis), `GET /health/live`, `GET /health/ready` * **Endpoints**: `GET /`, `GET /stats`, `GET /admins`, `GET /admins/count` * **Admin seeder**: Runs on startup; creates default admin from `ADMIN_EMAIL` / `ADMIN_PASSWORD` if none exists ## Key files ### `preview-config.yml` (repository root) ```yaml theme={null} framework: nestjs database: postgres health_check_path: /health app_port: 3000 app_port_env: PORT app_entrypoint: dist/main.js extra_services: - redis # Optional: startup_commands for migrations/seeding # startup_commands: # - npm run migration:run # - npm run seed ``` The orchestrator generates `docker-compose.preview.yml` from the NestJS template and wires the app to Postgres and Redis (e.g. `REDIS_URL=redis://redis:6379`). If the repo has no Dockerfile, a default NestJS Dockerfile can be injected from templates. ### Optional: custom `docker-compose.preview.yml` If you need custom services or overrides, you can add `docker-compose.preview.yml` (or `.yaml`) in the repo root. The orchestrator will use it and inject host ports for `app` and `db`. For a standard NestJS + Postgres + Redis setup, the built-in template is usually enough. ## Local run and API See the repo README for: * Prerequisites (Node.js 18+, PostgreSQL, Redis) * `pnpm install`, `pnpm run start` / `start:dev` / `start:prod` * Unit and e2e tests Main endpoints: `/health`, `/stats`, `/admins`, `/admins/count`. # Python Example Source: https://docs.prvue.dev/examples/python Example Python (FastAPI) repo using repo-owned docker-compose.preview.yml for preview-deployer This example uses **no built-in Python template**. The orchestrator uses a **repo-owned** `docker-compose.preview.yml` (or `docker-compose.preview.yaml`) in the repository root and injects host ports for the app and db so nginx can route correctly. ## Repository * **Repo**: [python-project-preview-example](https://github.com/your-org/python-project-preview-example) — replace with your repo URL when published. ## Stack * **Framework**: FastAPI (Python) * **Database**: PostgreSQL (async, SQLAlchemy) * **Cache**: Redis * **Config**: `.env` (`DATABASE_URL`, `REDIS_URL`) * **Health**: `GET /health` (DB + Redis status) * **Endpoints**: `GET /`, `GET /docs`, `GET /items`, `GET /admins`, etc. * **Seeder**: `python -m scripts.seed_admin` (optional, run separately or in startup) ## Key files ### `docker-compose.preview.yml` (repository root) — required For Python there is no built-in template, so you must provide a preview compose file. Use service names `app` and `db`; do **not** set host ports—the orchestrator injects them. Example structure (adjust image/build and env to match your repo): ```yaml theme={null} services: app: build: . ports: [] # Orchestrator injects host port environment: - DATABASE_URL=postgresql+asyncpg://preview:preview@db:5432/pr_${PR_NUMBER} - REDIS_URL=redis://redis:6379/0 depends_on: - db - redis db: image: postgres:16-alpine # No host ports; orchestrator injects environment: POSTGRES_USER: preview POSTGRES_PASSWORD: preview POSTGRES_DB: pr_${PR_NUMBER} redis: image: redis:7-alpine ``` The orchestrator writes `docker-compose.preview.generated.yml` with the injected ports and runs `docker compose` against that file. ### `preview-config.yml` (repository root) — required You must add `preview-config.yml` with all required fields. Set `framework: python` so the orchestrator knows the app port and entrypoint; when you provide `docker-compose.preview.yml`, the orchestrator still uses it for compose and uses `preview-config.yml` for validation, health check path, and optional build/startup commands. ```yaml theme={null} framework: python database: postgres health_check_path: /health app_port: 8000 app_port_env: PORT app_entrypoint: app.main:app # Optional: # build_commands: # - cp .env.example .env # startup_commands: # - python -m scripts.seed_admin # env: # - NODE_ENV=preview ``` ## Local run and API See the repo README for: * Venv, `pip install -r requirements.txt` * `DATABASE_URL`, `REDIS_URL` in `.env` * `python -m scripts.seed_admin`, `uvicorn app.main:app --reload --host 0.0.0.0 --port 8000` Endpoints: `/health`, `/docs`, `/items`, `/admins`, etc. # Rust Example Source: https://docs.prvue.dev/examples/rust Example Rust repo using repo-owned docker-compose.preview.yml for preview-deployer This example uses **no built-in Rust template**. The orchestrator uses a **repo-owned** `docker-compose.preview.yml` (or `docker-compose.preview.yaml`) in the repository root and injects host ports for the app and db. ## Repository * **Repo**: [rust-project-preview-example](https://github.com/your-org/rust-project-preview-example) — replace with your repo URL when published. ## Stack * **Framework**: Rust (Axum or similar) * **Database**: PostgreSQL (migrations on startup) * **Cache**: Redis * **Config**: `.env` (`DATABASE_URL`, `REDIS_URL`) * **Health**: `GET /health`, `GET /ready` (DB + Redis), `GET /ping`, `GET /stats` * **Migrations and seeder**: Run on app startup ## Key files ### `docker-compose.preview.yml` (repository root) — required For Rust there is no built-in template, so you must provide a preview compose file. Use service names `app` and `db`; do **not** set host ports—the orchestrator injects them. Example structure (adjust build and env to match your repo): ```yaml theme={null} services: app: build: . ports: [] # Orchestrator injects host port environment: - DATABASE_URL=postgres://preview:preview@db:5432/pr_${PR_NUMBER} - REDIS_URL=redis://redis:6379/ depends_on: - db - redis db: image: postgres:16-alpine environment: POSTGRES_USER: preview POSTGRES_PASSWORD: preview POSTGRES_DB: pr_${PR_NUMBER} redis: image: redis:7-alpine ``` The orchestrator writes `docker-compose.preview.generated.yml` with injected ports and runs `docker compose` against it. ### `preview-config.yml` (repository root) — required You must add `preview-config.yml` with all required fields. Set `framework: rust` so the orchestrator knows the app port and entrypoint; when you provide `docker-compose.preview.yml`, the orchestrator still uses it for compose and uses `preview-config.yml` for validation, health check path, and optional build/startup commands. ```yaml theme={null} framework: rust database: postgres health_check_path: /health app_port: 8080 app_port_env: PORT app_entrypoint: ./app # Or ./target/release/app for release builds # Optional: # build_commands: # - cp .env.example .env # env: # - RUST_LOG=info ``` ## Local run and API See the repo README for: * `cp .env.example .env`, set `DATABASE_URL` and `REDIS_URL` * `createdb preview_example`, `cargo run` * App listens on `http://0.0.0.0:8080` Endpoints: `/health`, `/ready`, `/ping`, `/stats`. Port and routing are handled by the orchestrator when using the injected compose. # Introduction Source: https://docs.prvue.dev/index Automated preview deployment for backend applications—isolated environments per PR on Digital Ocean Prvue creates **isolated preview environments** for backend applications when you open GitHub pull requests. Each PR gets its own Docker containers (app + database), path-based routing via nginx, and automatic cleanup when the PR closes or after a configurable TTL—similar to how Vercel or Netlify work for frontend apps. ## What you get * **Automatic previews**: Open a PR and get a live URL in 2–3 minutes * **Database isolation**: Each preview has its own PostgreSQL (or MySQL/MongoDB) instance * **Supported frameworks**: NestJS, Go, Laravel, Rust, and Python with built-in templates (or use repo-owned `docker-compose.preview.yml` for custom needs) * **Infrastructure as Code**: Terraform provisions the droplet; Ansible configures Docker, nginx, and the orchestrator * **CLI**: `preview init`, `preview setup`, `preview sync`, `preview status`, `preview destroy` ## Quick links Get Prvue up and running in minutes—prerequisites, tokens, and first PR. Repository config, CLI config, env vars, and framework options. System design, components, data flow, and security. NestJS, Laravel, Go, Python, and Rust example repos and key files. ## Next steps Follow the [Quickstart](/quickstart) to install the CLI, get tokens, and run `preview setup`. Add a `preview-config.yml` (and optional `docker-compose.preview.yml`) to your repository root. See [Configuration](/configuration) and [Examples](/examples). Create a branch, push, and open a PR. The orchestrator will build and comment with the preview URL. Example backends (NestJS, Laravel, Go, Python, Rust) are documented under **Examples** with repo links and key files. # Quickstart Source: https://docs.prvue.dev/quickstart Get Prvue up and running in minutes Get Prvue up and running in minutes. ## Prerequisites Before you begin, ensure you have: 1. **Digital Ocean Account**: Sign up at [digitalocean.com](https://www.digitalocean.com) 2. **GitHub Account**: With access to repositories you want to enable 3. **Node.js**: Version 20 or higher ([download](https://nodejs.org)) 4. **pnpm**: Package manager ([install](https://pnpm.io/installation)) 5. **Terraform**: Version 1.5 or higher ([install](https://www.terraform.io/downloads)) 6. **Ansible**: Version 2.14 or higher ([install](https://docs.ansible.com/ansible/latest/installation_guide/index.html)) 7. **SSH Key**: For accessing the Digital Ocean droplet ## Step 1: Install Prvue Clone the repository and install dependencies: ```bash theme={null} git clone https://github.com/danLeBrown/prvue.git cd prvue pnpm install pnpm build ``` ## Step 2: Get Required Tokens ### Digital Ocean API Token 1. Go to [Digital Ocean API Tokens](https://cloud.digitalocean.com/account/api/tokens) 2. Click "Generate New Token" 3. Give it a name (e.g., "preview-deployer") 4. Select "Write" scope 5. Copy the token (you won't see it again!) ### GitHub Personal Access Token 1. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) 2. Click "Generate new token (classic)" 3. Give it a name (e.g., "preview-deployer") 4. Select scopes: * `repo` (Full control of private repositories) * `admin:repo_hook` (Full control of repository hooks) 5. Generate and copy the token ## Step 3: Initialize Configuration Run the init command: ```bash theme={null} pnpm --filter cli run build preview init ``` You'll be prompted for: * Digital Ocean API token * Digital Ocean region (default: nyc3) * Droplet size (default: s-2vcpu-4gb) * GitHub personal access token * GitHub repositories (comma-separated, format: owner/repo) * Cleanup TTL in days (default: 7) * Max concurrent previews (default: 10) Configuration is saved to `~/.preview-deployer/config.yml`. Sensitive values are stored in your OS keychain. ## Step 4: Prepare Your Repository Add a `preview-config.yml` file to your repository root: ```yaml theme={null} framework: nestjs # or go, laravel, rust, python database: postgres health_check_path: /health app_port: 3000 app_port_env: PORT app_entrypoint: dist/main.js # NestJS example; value is framework-dependent ``` See [Configuration Reference](/configuration) for all options. The `app_entrypoint` value depends on your framework (e.g. Go: `server`, Python: `app.main:app`). Ensure your application has a health check endpoint at the specified path. ## Step 5: Set Up Infrastructure Run the setup command: ```bash theme={null} preview setup ``` This will: 1. Provision a Digital Ocean droplet 2. Configure the server with Docker, nginx, and the orchestrator 3. Create GitHub webhooks for your repositories You'll be prompted for: * SSH public key (for droplet access) * Confirmation to create infrastructure The setup process takes about 5–10 minutes. ## Step 6: Test It Out 1. Create a new branch in your repository 2. Make some changes 3. Open a pull request 4. Wait 2–3 minutes for the preview to deploy 5. Check the PR comments for the preview URL The preview URL will be in the format: `http://YOUR_SERVER_IP/{projectSlug}/pr-{PR_NUMBER}/` (e.g. `http://YOUR_SERVER_IP/myorg-myapp/pr-12/`). ## Step 7: Verify Deployment Check the status: ```bash theme={null} preview status ``` This shows: * Infrastructure status * Orchestrator health * Active preview deployments ## Updating the orchestrator If you change orchestrator code in the prvue repo (e.g. after pulling changes or editing the TypeScript), run: ```bash theme={null} preview sync ``` This builds the orchestrator locally, rsyncs it to the server, and restarts the orchestrator service. Use it after initial setup whenever you want to deploy orchestrator changes without re-running `preview setup`. ## Troubleshooting If something goes wrong: 1. Check the orchestrator logs: ```bash theme={null} ssh root@YOUR_SERVER_IP tail -f /opt/preview-deployer/logs/orchestrator.log # Or: journalctl -u preview-orchestrator -f ``` 2. Check Docker containers: ```bash theme={null} ssh root@YOUR_SERVER_IP docker ps -a docker logs {projectSlug}-pr-123-app # Replace with your project slug and PR number ``` 3. Check nginx configuration: ```bash theme={null} ssh root@YOUR_SERVER_IP nginx -t cat /etc/nginx/preview-configs/{projectSlug}-pr-123.conf ``` See [Troubleshooting Guide](/troubleshooting) for more help. ## Next Steps * Read the [Architecture Documentation](/architecture) to understand how it works * Customize your [Configuration](/configuration) * Set up monitoring and alerts * Configure custom domains (future feature) ## Cost Estimation Default setup costs approximately: * Digital Ocean droplet (s-2vcpu-4gb): \~\$24/month * Reserved IP: Free * Firewall: Free * Monitoring: Free **Total: \~\$24/month** You can reduce costs by: * Using a smaller droplet (s-1vcpu-2gb: \~\$12/month) * Using a cheaper region * Disabling backups ## Cleanup To destroy all infrastructure: ```bash theme={null} preview destroy ``` This will: 1. Cleanup all preview deployments 2. Delete GitHub webhooks 3. Destroy the Digital Ocean droplet This is irreversible! # Agent Handoff Prompt Source: https://docs.prvue.dev/reference/agent-handoff-prompt Cursor/agent handoff prompt for new sessions Copy the block below into a new Cursor agent (or chat) session so the agent uses the project's standards, plan, and docs before implementing anything. *** **Prompt to paste:** ``` You are working on the Prvue project. Before implementing any new features or changes: 1. Read these project artifacts in order: - README.md (project overview, structure, quick start) - docs/implementation-plan.md (canonical plan: architecture, implementation notes, port allocation, routing, nginx structure) - docs/architecture.md (components, data flow, security) - docs/configuration.md (config and env reference) - docs/quickstart.md and docs/troubleshooting.md as relevant 2. Follow the Cursor rules in .cursor/rules/ (especially preview-deployer-standards.mdc and typescript-and-build.mdc when editing TypeScript). They reflect the implementation plan and coding standards. 3. Conventions to respect: - pnpm monorepo (orchestrator + cli); TypeScript strict mode - Project slug from repo owner/name; deployment id {projectSlug}-{prNumber}; path-based routing /{projectSlug}/pr-{number}/ - Port allocation: global pool (next free app port from 8000, db from 9000) - Nginx: preview configs are included inside a default server block, not at http level - After code changes: run `pnpm build` and confirm CLI/orchestrator still work 4. If the task touches Terraform, Ansible, nginx, or deployment tracking, re-check the plan and docs for the correct structure (e.g. nginx server block, Ansible orchestrator source, deployment tracker sync vs async I/O). Do not skip reading the plan and rules; they define our standards. Then proceed with the requested task. ``` *** Use this when starting a new session so the agent is aware of the plan, rules, README, and docs before making changes. Documentation lives in the **docs.prvue.dev** repo (Mintlify). The paths above refer to the **prvue** repo; equivalent content is available at this docs site (e.g. [Quickstart](/quickstart), [Architecture](/architecture), [Configuration](/configuration)). # Documentation Guide Source: https://docs.prvue.dev/reference/documentation-guide Rules and standards for generating and maintaining Prvue documentation This guide is the single source of rules and standards for generating and maintaining the documentation. Anyone (humans or AI) contributing to the docs should follow it. ## For agents and new contributors If you are an AI agent or new contributor starting a session in the **docs.prvue.dev** repo, read the **Agent guide** first: open **`.cursor/agent-guide.md`** in the repo root. It summarizes: * What docs.prvue.dev is (documentation repo for Prvue) * What was done to set up the Mintlify site (steps, structure, two-repo split) * Where files live and which files to use (docs.json, llm.txt, documentation-guide, .cursor/rules.md) * How to add or change content and keep nav and llm.txt in sync Then use this Documentation guide for structure and writing rules, and `.cursor/rules.md` for Mintlify components and frontmatter. ## Overview * **Site**: Mintlify. Content is MDX; navigation is defined in `docs.json`. * **Coverage**: All existing docs (quickstart, configuration, architecture, testing, troubleshooting, orchestrator-how-it-works, implementation-plan, agent-handoff-prompt) are represented. New content must be added to the nav in `docs.json`. ## Structure * **Root-level pages**: quickstart, configuration, architecture, testing, troubleshooting (user-facing). * **examples/**: index + one page per example repo (NestJS, Laravel, Go, Python, Rust). New example repos get a new page and an entry in the Examples nav. * **reference/**: Contributor/agent-focused content: orchestrator-how-it-works, implementation-plan, agent-handoff-prompt, and this documentation guide. New reference pages go under `reference/` and are added to the Reference nav in `docs.json`. * **New guides**: Add at root or under a new group in `docs.json` as appropriate. ## Examples section standards * **One page per example repo.** Include: repo name and link, framework/stack. All five (NestJS, Laravel, Go, Rust, Python) are **supported** with built-in templates; **custom compose** (repo-owned `docker-compose.preview.yml`) is for other stacks or when you need custom services. * **Key files**: `preview-config.yml`, optional `docker-compose.preview.yml` or `.yaml`. Use code blocks for config snippets; link to repo README for local run and endpoints. * **Placeholder URLs**: When repos are private or not yet published, use placeholder URLs and document "Replace with your repo URL" until public. ## Reference section * **Audience**: Contributors and AI agents. Keep tone and audience clear. * **Content**: Orchestrator internals, implementation plan, agent handoff prompt, and this documentation guide. ## Internal links * Use Mintlify paths (e.g. `/configuration`, `/examples/nestjs`, `/reference/implementation-plan`). Do not use `.md` or `.mdx` file paths. * When moving or renaming pages, update internal links and `docs.json` navigation. ## MDX and frontmatter * Migrate from `.md` to `.mdx` when adding or editing pages. * **Frontmatter**: Every page must have a YAML frontmatter block with `title` and `description`. Follow the Mintlify and technical writing rules in this repo's `.cursor/rules.md`. * **Mermaid**: Supported; reuse diagrams from architecture and orchestrator-how-it-works where useful. * **Optional**: Meta block for SEO/sidebar; use Mintlify components (Card, Steps, Note, Warning, etc.) per the docs repo rules. ## llm.txt * **Location**: `llm.txt` in the docs repo root (or as specified in the project). It provides a plain-language "map" of the project and doc site for AI tools. * **When to update**: When adding new sections, new example repos, or restructuring the docs. Keep it in sync so AI context stays accurate. Mention this in contributions so contributors know to update it. ## When to update * **New doc page**: Create the page, add it to `docs.json` nav, and update `llm.txt` if the site map changes. * **New example repo**: Add a page under `examples/`, add to Examples nav, update examples index, and update `llm.txt`. * **New nav group**: Add the group and pages in `docs.json`; update `llm.txt` if the high-level structure changes. This guide lives in the docs so it can be updated as practices evolve and ensures consistent, discoverable rules for generating documentation. # Implementation Plan Source: https://docs.prvue.dev/reference/implementation-plan Canonical implementation plan for contributors and agents This is the canonical implementation plan for the project. It is synced from the Cursor plan and committed so all contributors and agents use the same reference. ## Architecture Overview The system follows a layered architecture: ``` GitHub Webhook → Orchestrator API → Docker Containers → Nginx Reverse Proxy → Preview URLs ``` **Key Components:** * **Terraform**: Provisions Digital Ocean droplet with networking and security * **Ansible**: Configures server with Docker, nginx, and orchestrator service * **Orchestrator**: TypeScript service handling webhooks, Docker management, nginx config, and cleanup * **CLI**: User-facing tool for setup, management, and teardown * **Templates**: Docker Compose and nginx config templates for preview environments ## Project Structure ``` preview-deployer/ ├── terraform/ # Infrastructure as Code ├── ansible/ # Server configuration ├── orchestrator/ # Core deployment service ├── cli/ # Command-line interface ├── templates/ # User-facing templates ├── docs/ # Documentation └── scripts/ # Utility scripts ``` ## Implementation Notes (Lessons Learned) * **Workspace TypeScript**: Package `tsconfig.json` should use `"extends": "../tsconfig.json"` (one level up from the package), not `"../../tsconfig.json"`. * **Deployment tracker I/O**: Use **sync** `fs` for hot paths (`getDeployment`, `getAllDeployments`, `allocatePorts`); **async** `fs/promises` for persistence. * **Dockerode types**: No maintained `@types/dockerode`. Use a local declaration or `// @ts-ignore`; document in orchestrator README. * **Optional native deps**: `keytar` may fail to build; keychain storage is optional; fallback to config file is acceptable for v1. * **Strict TypeScript**: Watch for variables used before assignment, "not all code paths return a value" in route handlers, and unused parameters (prefix with `_`). * **Nginx**: Preview configs must be included **inside** a default `server { }` block; `location` blocks are not valid at `http` level. ## Key Implementation Details * **Project slug**: Derived from repo `owner/name` (e.g. `myorg-myapp`). Used to avoid collisions when multiple repos have the same PR number. * **Deployment id**: `{projectSlug}-{prNumber}` (e.g. `myorg-myapp-12`). Single key for tracker, nginx config filenames, and compose project name. * **Port allocation**: Global pool; next free app port from 8000, next free db port from 9000. Keyed by deployment id. Allocations are stored in the deployment store's `portAllocations` map and released on cleanup so ports are reused correctly. Allocation excludes host ports currently bound by running Docker containers (so failed deployments whose containers still run do not cause port collisions). * **Routing**: Path-based `/{projectSlug}/pr-{number}/`; nginx proxies to `http://localhost:{appPort}/`. * **Deployment tracking**: JSON file at `/opt/preview-deployer/deployments.json`; keys are deployment ids; atomic file operations. For the full plan (phases, roles, validation checkpoints, testing), see the Cursor plan or the rest of this doc. This file is the single source of truth for architecture and implementation standards. # Orchestrator API Source: https://docs.prvue.dev/reference/orchestrator-api HTTP endpoints exposed by the orchestrator for health checks, webhooks, and debugging The orchestrator is an Express service that listens on **ORCHESTRATOR\_PORT** (default **3000**). It is typically not exposed publicly: nginx proxies `/webhook/github` to it; other endpoints are for health checks and debugging from the server (e.g. `curl` on the droplet or via SSH). **Base URL**: `http://localhost:3000` (on the server) or `http://SERVER_IP:3000` if the port is forwarded. Replace `SERVER_IP` with your droplet IP when running `curl` from your machine (only if port 3000 is reachable; by default it is internal). **OpenAPI / Swagger UI**: The API spec is available at `GET /openapi.json` and interactive docs at `GET /api-docs` for exploration (e.g. `http://localhost:3000/openapi.json`, `http://localhost:3000/api-docs`). ## Endpoints ### GET /health Liveness check. Returns basic orchestrator status and uptime. **Request**: No body or headers required. **Response** (200 OK): ```json theme={null} { "status": "ok", "timestamp": "2026-02-04T12:00:00.000Z", "uptime": 3600.5 } ``` **Example** (from server): ```bash theme={null} curl http://localhost:3000/health ``` Use this to confirm the orchestrator process is running (e.g. in [Troubleshooting](/troubleshooting)). *** ### POST /webhook/github Receives GitHub pull request webhook events. Validates the request signature, then triggers deploy, update, or cleanup. **Headers**: | Header | Required | Description | | --------------------- | -------- | ----------------------------------------------------- | | `Content-Type` | Yes | `application/json` | | `X-Hub-Signature-256` | Yes | HMAC SHA256 of the raw body using your webhook secret | **Body**: GitHub [pull\_request](https://docs.github.com/en/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request) webhook payload (JSON). Relevant fields include `action` (`opened`, `synchronize`, `closed`, `reopened`), `pull_request`, and `repository`. **Response**: * **200 OK**: `{ "status": "ok" }` — webhook accepted and processed (or queued). * **401 Unauthorized**: `{ "error": "Invalid signature" }` — `X-Hub-Signature-256` missing or does not match. * **500 Internal Server Error**: `{ "error": "" }` — processing failed (e.g. clone, build, or cleanup error). **Example** (from server; replace secret and payload): ```bash theme={null} curl -X POST http://localhost:3000/webhook/github \ -H "Content-Type: application/json" \ -H "X-Hub-Signature-256: sha256=" \ -d '{"action":"opened","pull_request":{...},"repository":{...}}' ``` In production, GitHub sends requests to `http://YOUR_SERVER_IP/webhook/github`; nginx proxies to the orchestrator. Do not call this endpoint manually unless you are replaying or testing webhooks with a valid signature. *** ### GET /api/previews Returns all tracked preview deployments. Useful for debugging: see which deployments exist, their status, ports, and URLs. **Request**: No body or headers required. **Response** (200 OK): ```json theme={null} { "deployments": [ { "prNumber": 12, "repoName": "my-app", "repoOwner": "my-org", "projectSlug": "my-org-my-app", "deploymentId": "my-org-my-app-12", "branch": "feature-branch", "commitSha": "abc123...", "framework": "nestjs", "dbType": "postgres", "appPort": 3000, "exposedAppPort": 8012, "exposedDbPort": 9012, "status": "running", "createdAt": "2026-02-04T10:00:00.000Z", "updatedAt": "2026-02-04T10:05:00.000Z", "url": "http://SERVER_IP/my-org-my-app/pr-12/", "commentId": 456789 } ] } ``` **Response** (500): `{ "error": "" }` — e.g. tracker read failed. **Example** (from server): ```bash theme={null} curl http://localhost:3000/api/previews ``` *** ### DELETE /api/previews/:deploymentId Manually cleanup a single preview: stops and removes Docker containers, removes nginx config, and deletes the deployment from the tracker. Use for debugging stuck previews or reclaiming resources. **Parameters**: | Parameter | Location | Description | | -------------- | -------- | --------------------------------------------------------------------------------- | | `deploymentId` | path | Full deployment id (e.g. `my-org-my-app-12`). Format: `{projectSlug}-{prNumber}`. | **Response**: * **200 OK**: `{ "status": "ok", "message": "Preview cleaned up" }` * **400 Bad Request**: `{ "error": "Invalid deployment id" }` — missing or empty `deploymentId`. * **404 Not Found**: `{ "error": "Deployment not found" }` — no tracked deployment with that id. * **500 Internal Server Error**: `{ "error": "" }` — cleanup failed (Docker, nginx, or tracker). **Example** (from server; use a real `deploymentId` from `GET /api/previews`): ```bash theme={null} curl -X DELETE http://localhost:3000/api/previews/my-org-my-app-12 ``` This endpoint immediately tears down the preview. Use it when you need to force-clean a deployment (e.g. after a failed build or for testing). For normal cleanup, rely on PR close or TTL. *** ## Debugging tips 1. **Orchestrator not responding**: Check that the service is running (`journalctl -u preview-orchestrator -f`) and that **ORCHESTRATOR\_PORT** (default 3000) is correct. Call `GET /health` from the server. 2. **List current previews**: Use `GET /api/previews` to see all deployments, their `deploymentId`, `status`, and `exposedAppPort`/`exposedDbPort`. Compare with `docker ps` and nginx configs. 3. **Force-clean a preview**: If a preview is stuck or you need to free a port, use `DELETE /api/previews/:deploymentId` with the id from `GET /api/previews`. Then verify with `GET /api/previews` and `docker ps`. 4. **Webhook signature**: Manual `POST /webhook/github` calls must include a valid `X-Hub-Signature-256` header (HMAC SHA256 of the raw JSON body with your `GITHUB_WEBHOOK_SECRET`). See [GitHub webhook signature verification](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries). For more debugging steps, see [Troubleshooting](/troubleshooting). # How the Orchestrator Works Source: https://docs.prvue.dev/reference/orchestrator-how-it-works Webhooks, containers per preview, and how the pieces connect This doc explains how the orchestrator receives GitHub events, how containers are spun up per preview, and how the pieces connect. Diagrams use Mermaid. ## 1. Webhooks: GitHub Pushes (No Polling) **The orchestrator does not poll GitHub.** GitHub **pushes** events to the orchestrator over HTTP when PR-related events happen. ``` GitHub (on PR open/update/close) → HTTP POST → Your server → Orchestrator ``` * **No polling** → no polling interval and no rate-limit risk from repeated API calls for "check for new PRs." * **Rate limits** that do apply are the normal GitHub API limits when the orchestrator *calls back* GitHub (e.g. post/update PR comments, list webhooks). Those are standard REST limits; webhook *delivery* is pushed by GitHub and doesn't consume polling-style quota. ### How the webhook URL gets on GitHub (you didn't type it in) You don't manually add the webhook in GitHub's UI. The **CLI creates it via the GitHub API** when you run: ```bash theme={null} preview setup ``` During setup, after Terraform and Ansible run, the CLI: 1. Reads your config (including `github.repositories` and `github.webhook_secret`). 2. For each repo, calls GitHub's [Create a webhook](https://docs.github.com/en/rest/repos/webhooks#create-a-repository-webhook) API. 3. Sets the webhook URL to `http:///webhook/github` and the secret to your configured `webhook_secret`. 4. Subscribes to the `pull_request` event. So the "webhook link" is registered programmatically; you only provided the token (with `admin:repo_hook`) and the repo list during `preview init`. ### End-to-end webhook path ```mermaid theme={null} sequenceDiagram participant Dev participant GitHub participant Nginx participant Orchestrator Dev->>GitHub: Open/update/close PR GitHub->>Nginx: POST /webhook/github (payload + X-Hub-Signature-256) Nginx->>Orchestrator: proxy to localhost:3000 Orchestrator->>Orchestrator: Verify HMAC, validate repo Orchestrator->>Orchestrator: handleWebhook(opened|synchronize|closed) ``` * **Nginx** listens on port 80 and proxies `/webhook/github` to the orchestrator (e.g. `localhost:3000`). * **Orchestrator** (Express) serves `POST /webhook/github`, verifies the signature with `GITHUB_WEBHOOK_SECRET`, then calls `WebhookHandler.handleWebhook(payload)`. ## 2. High-level architecture ```mermaid theme={null} flowchart LR subgraph GitHub PR[PR events] end subgraph Your server Nginx[Nginx :80] Orch[Orchestrator :3000] Docker[Docker] NC[/etc/nginx/preview-configs/] end PR -->|POST /webhook/github| Nginx Nginx --> Orch Orch -->|clone, build, up| Docker Orch -->|write pr-N.conf| NC Nginx -->|/pr-N/ → localhost:8xxx| Docker ``` * **Inbound:** GitHub → Nginx (80) → Orchestrator (3000). * **Orchestrator** drives clone/build/run via Docker and writes per-PR nginx configs. * **Traffic to previews:** Browser → Nginx `/pr-{number}/` → proxy to container port (8000 + prNumber). ## 3. Spinning up containers per preview When the webhook payload has `action: opened` or `reopened`, the orchestrator runs a **deploy** flow. When `action: synchronize`, it runs an **update** flow (same host, rebuild/restart). Containers are **one set per PR** (app + DB), not shared. **Required config:** The repo must have `preview-config.yml` at the root with all required fields: `framework`, `database`, `health_check_path`, `app_port`, `app_port_env`, `app_entrypoint`. Missing or invalid config fails the deployment. See [Configuration Reference](/configuration). ### Deploy flow (new PR) ```mermaid theme={null} flowchart TB A[handleDeploy] --> B[Post 'building' comment to PR] B --> C[Tracker.allocatePorts: app 8000+, db 9000+] C --> D[Create work dir: deployments/pr-N] D --> E[git clone + checkout branch + reset to commitSha] E --> F[Load and validate preview-config.yml] F --> G[Generate or use repo docker-compose.preview.yml] G --> H[docker compose up -d --build] H --> I[Wait for health_check_path on app port] I --> J[NginxManager.addPreview: write pr-N.conf, reload nginx] J --> K[Tracker.saveDeployment] K --> L[Update PR comment with preview URL] ``` * **Ports:** Allocated from global pool (app from 8000, db from 9000); stored in `portAllocations` and released on cleanup (see [Implementation plan](/reference/implementation-plan)). * **Work dir:** `/opt/preview-deployments//pr-/` (or `DEPLOYMENTS_DIR`); clone lives there, and `docker compose` runs in that directory. * **Compose file:** Generated from Handlebars templates per framework (NestJS, Go, Laravel, Rust, Python) or from repo-owned `docker-compose.preview.yml`; uses `app_port`, `app_port_env`, `app_entrypoint` from `preview-config.yml`. * **Health:** Orchestrator polls `http://localhost:` (e.g. every 5s, up to 60s) before marking the preview as up and updating the PR comment. ### Container layout per PR ```mermaid theme={null} flowchart LR subgraph "Server" subgraph "PR #123" A[pr-123-app :8123] B[pr-123-db :9123] end subgraph "PR #124" C[pr-124-app :8124] D[pr-124-db :9124] end end Nginx[Nginx :80] Nginx -->|/pr-123/| A Nginx -->|/pr-124/| C ``` * Each PR gets its own app and DB containers and ports; no sharing between PRs. * Nginx includes configs from `/etc/nginx/preview-configs/`; each file defines `location /pr-/` and `proxy_pass` to the corresponding app port. ### Update and cleanup * **Update (synchronize):** Same `pr-` work dir; `git fetch` + `git reset --hard `, then `docker compose up -d --build`, health check, update deployment record and PR comment. * **Cleanup (closed or TTL):** `docker compose down -v`, remove work dir, remove nginx config, reload nginx, `tracker.deleteDeployment` and release ports. ## 4. Where it lives in the repo | Concern | Where | | ----------------------------------- | -------------------------------------------------------------------------------------------------------- | | Webhook HTTP endpoint | `orchestrator/src/index.ts` → `POST /webhook/github` | | Signature check + routing by action | `orchestrator/src/webhook-handler.ts` | | Clone, build, compose, health | `orchestrator/src/docker-manager.ts` | | Per-PR nginx config + reload | `orchestrator/src/nginx-manager.ts` | | Creating the webhook on GitHub | `cli/src/commands/setup.ts` + `cli/src/utils/github.ts` | | Nginx proxy for `/webhook/github` | `ansible/roles/nginx/templates/nginx.conf.j2` (default server: `location /webhook/` → orchestrator port) | # Testing Source: https://docs.prvue.dev/testing Running tests, coverage, and guidelines for writing tests in Prvue Prvue uses **Jest** for the orchestrator package. Tests are co-located with source (e.g. `project-slug.test.ts` next to `project-slug.ts`); E2E tests live under `orchestrator/tests/e2e/`. ## First-time test setup E2E tests load env from **`orchestrator/.env.test`** (not `.env`). That file is not committed. Copy the example and fill in values as needed: ```bash theme={null} cp orchestrator/.env.test.example orchestrator/.env.test # Edit orchestrator/.env.test: set GITHUB_TOKEN, GITHUB_WEBHOOK_SECRET, ALLOWED_REPOS, PREVIEW_BASE_URL for full E2E; optionally DOCKER_TEST_REPO_URL for DockerManager integration. ``` ## Running tests From the repo root: ```bash theme={null} # Orchestrator unit tests (default) pnpm --filter @prvue/orchestrator run test:unit # Unit + integration (no extra env; integration uses temp dirs, no nginx binary) pnpm --filter @prvue/orchestrator run test:all # Integration only (Docker must be running for DockerManager tests) pnpm --filter @prvue/orchestrator run test:integration # E2E API tier (no GitHub/Docker; mocked services) pnpm --filter @prvue/orchestrator run test:e2e # With coverage pnpm --filter @prvue/orchestrator run test:coverage # Watch mode pnpm --filter @prvue/orchestrator run test:watch ``` From `orchestrator/`: ```bash theme={null} pnpm test:unit pnpm test:all # unit + integration pnpm test:integration # requires Docker for DockerManager suite pnpm test:e2e # API tier always runs; full tier only when E2E_FULL=1 pnpm test:coverage pnpm test:watch ``` ## Test layers * **Unit** (`*.test.ts`): Fast, no external services. Mocks fs, logger, etc. Run by default with `test:unit`. Excluded from build output. * **Integration** (`*.integration.test.ts`): Real file I/O; NginxManager tests use a temp dir and no-op reload (no nginx binary). DockerManager integration tests run only when `DOCKER_TEST_REPO_URL` is set (see below). * **E2E** (`tests/e2e/*.e2e.test.ts`): * **API tier** (`webhook-api.e2e.test.ts`): Full HTTP stack with mocked GitHub and Docker. No extra env; always runs with `test:e2e`. * **Full tier** (`webhook-full.e2e.test.ts`): Real GitHub client, real Docker, real clone and deploy. Skipped unless `E2E_FULL=1` or `CI_FULL_E2E=1`. Requires `GITHUB_TOKEN`, `GITHUB_WEBHOOK_SECRET`, `ALLOWED_REPOS`, `PREVIEW_BASE_URL`, and a test repo with a minimal Dockerfile and health endpoint. Integration and E2E are excluded from `test:unit`. ## Guidelines for writing tests Use these so tests stay consistent and other agents can follow the same patterns. ### Layout and naming * **Unit / integration**: Co-locate with source. Suffixes: `*.test.ts` (unit), `*.integration.test.ts` (integration). E2E: `orchestrator/tests/e2e/*.e2e.test.ts`. * **Descriptive names**: Test names should describe behavior or outcome, not implementation (e.g. "should return 401 when signature is invalid", not "should test validation"). ### Unit tests * **Mock external deps**: Mock `fs`/`fs/promises`, logger, and other I/O so tests are fast and deterministic. * **Mocking `fs/promises`**: Use a Jest factory so you can set implementations: `jest.mock('fs/promises', () => ({ access: jest.fn(), readFile: jest.fn() }))`. In tests use `fsMock.access.mockResolvedValue(...)` (do not reassign `fsMock.access = jest.fn()` — the module may have getter-only properties). * **Tracked state**: Use temp files/dirs for code that reads or writes the filesystem (e.g. deployment tracker). Create a unique path per run (e.g. `path.join(os.tmpdir(), 'prefix-' + Date.now() + '-' + Math.random().toString(36).slice(2))`) and clean up in `afterEach`/`afterAll`. ### Integration tests * **Temp dirs**: Use temp dirs for config, deployments, and DB paths; create and clean up in `beforeEach`/`afterEach` or `beforeAll`/`afterAll`. * **NginxManager**: Pass `reloadCommand: async () => {}` so tests don't need nginx or sudo. Assert only file contents and presence/absence of config files. * **DockerManager**: Run the suite only when `DOCKER_TEST_REPO_URL` is set (e.g. `const describeIfRepo = process.env.DOCKER_TEST_REPO_URL ? describe : describe.skip`). Save the deployment to the tracker after `deployPreview` so `cleanupPreview` can find the work dir. Clean up in `afterAll`. ### E2E tests * **Env**: E2E loads `orchestrator/.env.test` via `tests/setup-env.ts` (Jest e2e `setupFiles`). Do not rely on `.env` for E2E. * **Stopping the app**: If the test uses `createApp`, call `stopScheduledCleanup()` in `afterEach` so the cleanup interval doesn't keep the process alive and Jest can exit. Store the return value and call it in `afterEach`. * **Webhook signature**: Sign the exact string the server will verify. The server uses `JSON.stringify(req.body)`. In the test: build the payload object, then `bodyString = JSON.stringify(payload)`, sign `bodyString`, and send the same object with `.send(payload)` so the server's stringify matches. * **Invalid-signature tests**: `crypto.timingSafeEqual` requires same-length buffers. Use a same-length invalid value (e.g. `'sha256=' + '0'.repeat(64)`) so the handler can return 401 instead of throwing. * **Full E2E**: Skip unless `E2E_FULL=1` or `CI_FULL_E2E=1` (e.g. `const describeFull = runFullE2E ? describe : describe.skip`). Require env vars in `beforeAll` and fail fast with a clear message. ### General * **Arrange–Act–Assert**: Structure tests as setup, action, then assertions. * **One assertion per test** when it keeps tests clear; group related assertions in a single test when they describe one behavior. * **Edge cases**: Cover boundaries (e.g. first allocation, duplicate allocation, invalid input) and error paths. * **Production code**: Prefer dependency injection and small, pure functions so units can be tested without heavy mocking. ## Integration test details * **NginxManager**: Writes config to a temp dir; reload is a no-op. No nginx binary or sudo required. * **DockerManager**: Runs only when `DOCKER_TEST_REPO_URL` is set to a minimal public repo (e.g. one with a Dockerfile and `/health`). Clone, deploy, then cleanup. Example: `DOCKER_TEST_REPO_URL=https://github.com/owner/minimal-preview-app.git`. ## E2E full tier E2E tests load env from **`orchestrator/.env.test`** (via `tests/setup-env.ts`), not `.env`. Copy `orchestrator/.env.test.example` to `orchestrator/.env.test` and set the required vars. To run full E2E (real deploy and cleanup): ```bash theme={null} E2E_FULL=1 pnpm --filter @prvue/orchestrator run test:e2e ``` In `.env.test` set `GITHUB_TOKEN`, `GITHUB_WEBHOOK_SECRET`, `ALLOWED_REPOS` (e.g. `owner/repo`), and `PREVIEW_BASE_URL`. The test repo should have branch `main`, a Dockerfile, and a health endpoint (e.g. `/health`). ## Current coverage Unit tests cover: * **project-slug** (project-slug-util): `toProjectSlug`, `toDeploymentId` * **framework-detection**: NestJS/Go/Laravel detection, `resolveFramework` (mocked `fs/promises`) * **deployment-tracker**: `allocatePorts`, `releasePorts`, get/save/delete deployment, `getAllDeployments`, `getDeploymentAge` (temp file store, mock logger) Integration tests: * **NginxManager**: `addPreview` writes path-based config and `proxy_pass`; `removePreview` deletes the file. * **DockerManager** (when `DOCKER_TEST_REPO_URL` set): Deploy and cleanup with a real repo. E2E API tier: * Health, webhook (signed), list previews, delete preview, list empty; invalid signature returns 401. After code changes, run `pnpm build` and orchestrator unit tests before considering work done. # Troubleshooting Source: https://docs.prvue.dev/troubleshooting Common issues and solutions for Prvue Common issues and solutions for Prvue. ## Setup Issues ### Terraform Errors **Error: "Failed to initialize Terraform"** * Ensure Terraform is installed: `terraform version` * Check Terraform version >= 1.5.0 * Verify Digital Ocean token is valid **Error: "Failed to create droplet"** * Check Digital Ocean account has sufficient credits * Verify region is available * Check droplet size is available in selected region **Error: "SSH connection failed"** * Verify SSH public key is correct * Check firewall allows SSH (port 22) * Wait a few minutes for droplet to fully initialize ### Ansible Errors **Error: "Failed to connect to host"** * Ensure SSH access works: `ssh root@SERVER_IP` * Check inventory file is correctly generated * Verify SSH key is added to droplet **Error: "Docker installation failed"** * Check internet connectivity on droplet * Verify Ubuntu 22.04 is being used * Check Ansible logs for detailed error **Error: "Orchestrator service failed to start"** * Check environment variables are set correctly * Verify Node.js is installed: `node --version` * Check orchestrator logs: `journalctl -u preview-orchestrator -f` ## Deployment Issues ### Webhook Not Triggering **Symptoms**: PR opened but no preview deployment **Solutions**: 1. Check webhook is configured: ```bash theme={null} # Via GitHub API or web interface ``` 2. Verify webhook secret matches: ```bash theme={null} cat ~/.preview-deployer/config.yml | grep webhook_secret ``` 3. Check orchestrator logs: ```bash theme={null} ssh root@SERVER_IP journalctl -u preview-orchestrator -f ``` 4. Test webhook manually: ```bash theme={null} curl -X POST http://SERVER_IP:3000/webhook/github \ -H "Content-Type: application/json" \ -H "X-Hub-Signature-256: sha256=..." \ -d '{"action":"opened",...}' ``` ### Missing or Invalid `preview-config.yml` **Symptoms**: Deployment fails with "preview-config.yml is required at repository root but was not found" or a validation error (e.g. "framework is required", "health\_check\_path is not valid"). **Cause**: The orchestrator requires a valid `preview-config.yml` at the repository root. Required fields: `framework`, `database`, `health_check_path`, `app_port`, `app_port_env`, `app_entrypoint`. **Solutions**: 1. Add `preview-config.yml` in the repo root (see [Configuration Reference](/configuration)). 2. Ensure all required fields are present and valid (e.g. `health_check_path` must start with `/`, `app_port` must be a positive number). 3. If the file exists but deployment still fails, check orchestrator logs for the exact validation message (e.g. invalid YAML or missing field). ### Build Failures **Symptoms**: Preview deployment fails during build **Solutions**: 1. Check Docker build logs: ```bash theme={null} ssh root@SERVER_IP # Work dir is {projectSlug}/pr-{PR_NUMBER} (e.g. myorg-myapp/pr-12) cd /opt/preview-deployments/{projectSlug}/pr-{PR_NUMBER} docker compose -p {deploymentId} logs app ``` 2. Verify Dockerfile exists and is correct 3. Check build commands in `preview-config.yml` 4. Ensure dependencies are installable #### `pnpm install --prod` fails (e.g. prepare script / husky) **Symptoms**: Build fails at `RUN pnpm install --frozen-lockfile --prod` (or similar) with exit code 1. **Cause**: Lifecycle scripts like `prepare` or `postinstall` in `package.json` can depend on devDependencies (e.g. **husky**). With `--prod`, devDependencies are not installed, so the script fails when it runs. **Fixes (in your app or its Dockerfile)**: * **Husky**: Set `ENV HUSKY=0` before the install step in your Dockerfile so the prepare script is a no-op in Docker/CI. * **General**: Move the script's dependency to `dependencies` if it must run in production builds, or in the Dockerfile run install with `--ignore-scripts` and then run only the commands you need (e.g. build). * **Prvue**: When the repo has no Dockerfile, we inject a default that sets `HUSKY=0` so this case is avoided for our template. ### Health Check Failures **Symptoms**: Containers start but preview URL doesn't work **Solutions**: 1. Verify health check endpoint exists: ```bash theme={null} curl http://localhost:{APP_PORT}/health ``` 2. Check health check path in `preview-config.yml`: ```yaml theme={null} health_check_path: /health # Must match your app's endpoint ``` 3. Check container logs: ```bash theme={null} docker logs {projectSlug}-pr-{PR_NUMBER}-app ``` 4. Verify app is listening on correct port: ```bash theme={null} docker exec {projectSlug}-pr-{PR_NUMBER}-app netstat -tlnp ``` ### Port Conflicts **Symptoms**: "Port already in use" error **Solutions**: 1. Check allocated ports: ```bash theme={null} ssh root@SERVER_IP cat /opt/preview-deployer/deployments.json | jq '.portAllocations' ``` 2. Find process using port: ```bash theme={null} lsof -i :{PORT} ``` 3. Cleanup old deployment (use the full deployment id from `GET /api/previews`, e.g. `my-org-my-app-12`): ```bash theme={null} curl -X DELETE http://SERVER_IP:3000/api/previews/{deploymentId} ``` See [Orchestrator API](/reference/orchestrator-api) for full endpoint details. ### Nginx Configuration Errors **Symptoms**: Preview URL returns 502 Bad Gateway **Solutions**: 1. Check nginx config syntax: ```bash theme={null} ssh root@SERVER_IP nginx -t ``` 2. Verify preview config exists: ```bash theme={null} cat /etc/nginx/preview-configs/{projectSlug}-pr-{PR_NUMBER}.conf ``` 3. Check nginx error logs: ```bash theme={null} tail -f /var/log/nginx/error.log ``` 4. Verify app container is running: ```bash theme={null} docker ps | grep {projectSlug}-pr-{PR_NUMBER} ``` 5. Test proxy directly: ```bash theme={null} curl -H "Host: SERVER_IP" http://localhost/{projectSlug}/pr-{PR_NUMBER}/ ``` ## Runtime Issues ### Container Crashes **Symptoms**: Preview works initially but stops responding **Solutions**: 1. Check container status: ```bash theme={null} docker ps -a | grep {projectSlug}-pr-{PR_NUMBER} ``` 2. View container logs: ```bash theme={null} docker logs {projectSlug}-pr-{PR_NUMBER}-app ``` 3. Check resource usage: ```bash theme={null} docker stats {projectSlug}-pr-{PR_NUMBER}-app ``` 4. Restart container: ```bash theme={null} cd /opt/preview-deployments/{projectSlug}/pr-{PR_NUMBER} docker compose restart app ``` ### Database Connection Issues **Symptoms**: App can't connect to database **Solutions**: 1. Verify database container is running: ```bash theme={null} docker ps | grep {projectSlug}-pr-{PR_NUMBER}-db ``` 2. Check database logs: ```bash theme={null} docker logs {projectSlug}-pr-{PR_NUMBER}-db ``` 3. Test database connection: ```bash theme={null} docker exec {projectSlug}-pr-{PR_NUMBER}-db pg_isready -U preview ``` 4. Verify connection string in app: ```bash theme={null} docker exec {projectSlug}-pr-{PR_NUMBER}-app env | grep DATABASE_URL ``` ### Cleanup Not Working **Symptoms**: Old previews not being cleaned up **Solutions**: 1. Check cleanup service is running: ```bash theme={null} ssh root@SERVER_IP journalctl -u preview-orchestrator | grep cleanup ``` 2. Verify TTL configuration: ```bash theme={null} cat ~/.preview-deployer/config.yml | grep cleanup_ttl_days ``` 3. Manually trigger cleanup (use deployment id from `GET /api/previews`, e.g. `my-org-my-app-12`): ```bash theme={null} curl -X DELETE http://SERVER_IP:3000/api/previews/{deploymentId} ``` 4. Check deployment age: ```bash theme={null} cat /opt/preview-deployer/deployments.json | jq '.deployments."{deploymentId}"' ``` ## Performance Issues ### Slow Builds **Solutions**: 1. Use Docker layer caching 2. Optimize Dockerfile (multi-stage builds) 3. Use smaller base images 4. Cache dependencies in separate layer ### High Resource Usage **Solutions**: 1. Reduce max concurrent previews 2. Lower container resource limits 3. Use smaller droplet size 4. Enable cleanup of old previews ### Memory Issues **Symptoms**: Droplet runs out of memory **Solutions**: 1. Check memory usage: ```bash theme={null} free -h docker stats ``` 2. Reduce container memory limits 3. Cleanup old previews 4. Upgrade droplet size ## Security Issues ### Webhook Signature Verification Failed **Solutions**: 1. Verify webhook secret matches: ```bash theme={null} cat ~/.preview-deployer/config.yml | grep webhook_secret ``` 2. Check GitHub webhook configuration 3. Verify payload is not modified ### Unauthorized Repository Access **Solutions**: 1. Check `ALLOWED_REPOS` environment variable 2. Verify repository format: `owner/repo` 3. Check orchestrator logs for rejection messages ## Debugging Tips ### Enable Debug Logging Set `LOG_LEVEL=debug` in the orchestrator environment (or set Ansible variable `orchestrator_log_level: debug`). Debug lines then appear in the same file as info logs: `/opt/preview-deployer/logs/orchestrator.log`. View them with: ```bash theme={null} tail -f /opt/preview-deployer/logs/orchestrator.log ``` To set via systemd override: ```bash theme={null} ssh root@SERVER_IP systemctl edit preview-orchestrator # Add: Environment="LOG_LEVEL=debug" systemctl daemon-reload systemctl restart preview-orchestrator ``` ### Check All Services ```bash theme={null} ssh root@SERVER_IP # Docker systemctl status docker docker ps # Nginx systemctl status nginx nginx -t # Orchestrator systemctl status preview-orchestrator tail -n 50 /opt/preview-deployer/logs/orchestrator.log # Or: journalctl -u preview-orchestrator -n 50 ``` ### Manual Testing Test orchestrator API (see [Orchestrator API](/reference/orchestrator-api) for full endpoint reference): ```bash theme={null} curl http://SERVER_IP:3000/health curl http://SERVER_IP:3000/api/previews ``` Test webhook: ```bash theme={null} # Generate signature SECRET="your-webhook-secret" PAYLOAD='{"action":"opened",...}' SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2) curl -X POST http://SERVER_IP:3000/webhook/github \ -H "Content-Type: application/json" \ -H "X-Hub-Signature-256: sha256=$SIGNATURE" \ -d "$PAYLOAD" ``` ## Getting Help If you're still stuck: 1. Check logs for error messages 2. Review [Architecture Documentation](/architecture) 3. Check [Configuration Reference](/configuration) 4. Open an issue on GitHub with: * Error messages * Logs (sanitized) * Steps to reproduce * System information ## Common Error Messages ### "Repository not in allowed list" * Add repository to `ALLOWED_REPOS` environment variable * Restart orchestrator service ### "Port allocation out of range" * PR number too large (>56,000) * Use smaller PR numbers or upgrade port allocation strategy ### "Health check timeout" * Verify health check endpoint exists * Check health check path in config * Increase timeout in docker-manager.ts ### "Docker build failed" * Check Dockerfile syntax * Verify all dependencies are available * Check build logs for specific errors ### "Nginx reload failed" * Check nginx config syntax: `nginx -t` * Verify preview config file format * Check nginx error logs