Skip to content

Feature: PR Preview Environments #21

@pablopunk

Description

@pablopunk

Overview

Create preview environments for PRs so that doce.dev can test changes before merging. Each PR should get a dedicated, isolated environment with full Docker support to build and run project containers.

Requirements

  • Only same-repo branches (no forks) - PRs from forks should not trigger previews
  • Multiple concurrent PR environments supported
  • Delete on close - clean up preview resources when PR is closed
  • Full Docker access - preview environments must be able to run docker and docker compose commands
  • Self-configured - admin user and OpenRouter API key are configured on first visit (not pre-configured in CI)

Architecture

Host Requirements (VPS)

  • Docker Engine + docker compose plugin
  • Node.js 20 + pnpm
  • Reverse proxy (Caddy recommended)
  • Wildcard DNS: *.preview.doce.dev → VPS IP (via Vercel DNS)

Per-PR Isolation

Each PR preview needs:

  • Directory: /opt/doce/previews/pr-{NUMBER}/ (full repo checkout at PR SHA)
  • Data: data/ subdirectory with isolated SQLite DB and project files
  • Port: Unique port in range 48000-49000 (persistent across PR updates)
  • Service: doce-preview@{NUMBER}.systemd unit
  • Subdomain: pr-{NUMBER}.preview.doce.dev

Key Technical Details

  • doce.dev shells out to docker compose directly (see src/server/docker/compose.ts)
  • Data paths are relative to process.cwd() (see src/server/projects/paths.ts)
  • DB path is configurable via DB_FILE_NAME env var (see src/server/db/client.ts)
  • Each preview runs as a separate Node process with isolated working directory

Implementation Checklist

1. VPS Bootstrap

  • Provision VPS with Docker, Node 20, pnpm
  • Install Caddy reverse proxy
  • Configure wildcard DNS in Vercel: A *.preview.doce.dev → <VPS_IP>
  • Create /opt/doce/previews directory structure

2. Process Management

  • Create systemd template unit: doce-preview@.service
  • Template should:
    • Use WorkingDirectory=/opt/doce/previews/pr-%i
    • Load env vars from /opt/doce/previews/pr-%i/.env
    • Run node dist/server/entry.mjs
    • Enable Restart=always

3. Port Allocation System

  • Create port registry (simple file-based: /opt/doce/previews/ports/)
  • Allocate first free port in range 48000-49000 for each new PR
  • Store allocated port in /opt/doce/previews/pr-{NUMBER}/PORT
  • Release port on PR close

4. Caddy Configuration

  • Create Caddyfile snippet pattern per PR
  • Example snippet for pr-123.preview.doce.dev:
    pr-123.preview.doce.dev {
        reverse_proxy 127.0.0.1:<allocated_port>
    }
    
  • Script to add/remove snippets and reload Caddy

5. GitHub Actions Workflow

Create .github/workflows/pr-preview.yml with:

Triggers:

  • pull_requestopened, reopened, synchronize (deploy)
  • pull_requestclosed (teardown)

Deploy job (same-repo only):

jobs:
  deploy-preview:
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    steps:
      - name: SSH and deploy
        run: |
          # 1. Ensure PR directory exists
          # 2. Fetch/update repo to PR SHA
          # 3. Allocate port (if first time)
          # 4. Write .env with PORT, HOST, DB_FILE_NAME
          # 5. pnpm install && pnpm build
          # 6. systemctl restart doce-preview@{PR_NUMBER}
          # 7. Add Caddy snippet and reload
          # 8. Comment PR with preview URL

Teardown job:

jobs:
  teardown-preview:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - name: SSH and teardown
        run: |
          # 1. systemctl stop doce-preview@{PR_NUMBER}
          # 2. Remove Caddy snippet and reload
          # 3. Release port
          # 4. Optionally delete PR directory

Secrets needed:

  • SSH_PRIVATE_KEY - for accessing the VPS
  • VPS_HOST - VPS IP/hostname
  • VPS_USER - SSH user (e.g., root or admin)

6. Environment Variables Per PR

Create .env file in each PR directory:

PORT=<allocated_port>
HOST=127.0.0.1
DB_FILE_NAME=/opt/doce/previews/pr-{NUMBER}/data/db.sqlite

7. Security Considerations

  • Only deploy previews for same-repo PRs (Docker access is powerful)
  • Preview services run on localhost only (127.0.0.1), accessed via reverse proxy
  • Preview host needs access to Docker daemon (add user to docker group)

Example Preview Flow

  1. PR #123 opened from branch feature/new-login
  2. GitHub Actions deploys to /opt/doce/previews/pr-123/
  3. Allocates port 48015, stores in PORT file
  4. Creates Caddy snippet → pr-123.preview.doce.dev
  5. Preview available at https://pr-123.preview.doce.dev
  6. User visits URL, configures admin + OpenRouter on first setup
  7. User can create projects, run OpenCode agents, build containers
  8. PR closed → preview stopped, Caddy snippet removed, port released

Files to Create/Modify

  • .github/workflows/pr-preview.yml - main workflow
  • scripts/deploy-preview.sh - helper script for SSH deploy (optional)
  • scripts/teardown-preview.sh - helper script for SSH teardown (optional)
  • systemd/doce-preview@.service - systemd unit template
  • caddy/Caddyfile.snippet.example - example Caddy snippet

Out of Scope (for now)

  • Fork PR previews
  • Pre-configured OpenRouter key
  • Custom domains per PR
  • Preview sharing via link token
  • Metrics/log aggregation across previews

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions