Preview Deployments Setup for Pull Requests
Preview Deployment — an automatically created temporary environment for each Pull Request with a unique URL like https://pr-123.preview.example.com. Reviewer opens a live site, clicks through the interface, checks changes without needing to clone the repo and set up local environment.
Platforms with Built-in Preview Deployments
Vercel — best option for Next.js and static sites. Preview deployments work out of the box when connecting GitHub repo. Each PR gets unique URL, link added to PR comment automatically.
Netlify — similar functionality for static sites and JAMstack. Deploy Previews enabled by default. Supports split testing between preview and production.
Railway / Render — for full-stack apps with database. Railway creates isolated environment with separate DB for each PR.
Custom Implementation on VPS
For apps that can't deploy to Vercel/Netlify (Docker containers, specific requirements):
# .github/workflows/preview.yml
name: Preview Deployment
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
deploy-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build -t app:pr-${{ github.event.pull_request.number }} .
- name: Deploy to preview server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PREVIEW_SERVER_HOST }}
username: deploy
key: ${{ secrets.PREVIEW_SSH_KEY }}
script: |
docker pull registry.example.com/app:pr-${{ github.event.pull_request.number }}
docker stop app-pr-${{ github.event.pull_request.number }} || true
docker run -d --name app-pr-${{ github.event.pull_request.number }} \
-p 0:3000 \
--label traefik.enable=true \
--label "traefik.http.routers.pr-${{ github.event.pull_request.number }}.rule=Host(\`pr-${{ github.event.pull_request.number }}.preview.example.com\`)" \
registry.example.com/app:pr-${{ github.event.pull_request.number }}
- name: Comment PR with preview URL
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview: https://pr-${context.issue.number}.preview.example.com`
})
Traefik automatically routes traffic to the right container by subdomain. Wildcard DNS record *.preview.example.com points to preview server.
Cleanup of Outdated Environments
# Remove preview when PR closed
on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Remove preview deployment
uses: appleboy/ssh-action@v1
with:
script: |
docker stop app-pr-${{ github.event.pull_request.number }}
docker rm app-pr-${{ github.event.pull_request.number }}
Database for Preview Environments
Options:
- Shared read-only database — fast, but can't test writes
- Separate DB per PR — full isolation, but requires resources. Neon (PostgreSQL) supports database branching: creates DB branch instantly via copy-on-write
- Seeded in-memory database — SQLite or PostgreSQL with fixed test data
Timeline
Preview Deployments on Vercel/Netlify — 0.5 day. Custom implementation with Docker, Traefik and cleanup — 2–3 days. Add database branching via Neon — 1 day.







