TempsTemps
  • Docs
  • Blog
  • Pricing
  • Enterprise
  • Security
  • Contact
Star—
TempsTemps

Open-source deployment platform with built-in error tracking, analytics, and monitoring. Runs on any VPS. No surprise bills, no data leaving your infrastructure.

  • Product
  • Features
  • Documentation
  • Changelog
  • Enterprise
  • Contact
  • Resources
  • Getting Started
  • Upgrade
  • GitHub
  • Reddit
  • Tools
  • VPS Security Scanner
  • PaaS Tax Calculator
  • Compare
  • vs Vercel
  • vs Netlify
  • vs Coolify
  • All Platforms
  • Deploy
  • Next.js
  • Node.js
  • Django
  • Laravel
  • Go
  • Rust
  • All Frameworks →
  • Legal & Compliance
  • Security & Trust
  • Data Ownership & Privacy
  • GDPR Compliance

© 2026 Temps. All rights reserved.

GitHubDocs
t
Temps

Temps: Every PR Gets Its Own URL in 30 Seconds — Preview Environments Without the YAML

Temps: Every PR Gets Its Own URL in 30 Seconds — Preview Environments Without the YAML

March 12, 2026 (4mo ago)

Temps Team

Written by Temps Team

Last updated July 5, 2026 (3w ago)

Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

#preview-environments#pull-request#ci-cd#github-actions#docker#preview environments pull request#preview environments per pull request#auto preview deployments 2026#best platforms for preview deployments
Back to all posts

What Is the Easiest Way to Set Up Preview Environments for Every Pull Request?

The easiest way to set up preview environments for every pull request is to use a platform that handles it natively. With Temps, you connect your GitHub repository and every pull request automatically gets a live preview environment at a unique URL — no GitHub Actions workflow to write, no reverse-proxy configuration, no wildcard DNS setup. Push a branch, open a PR, and the preview URL appears as a sticky comment within minutes. Temps auto-creates preview environments per pull request and updates them on every push.

If you want full control and prefer to build it yourself, you can do so in an afternoon with webhooks, Docker, and Traefik — but expect 2–4 hours of initial setup plus ongoing maintenance.


Temps vs Vercel vs Netlify: Preview Environment Comparison

FeatureTemps (self-hosted)VercelNetlify
Auto-deploy every PRYesYesYes
Scale-to-zero (on-demand mode)Yes — configurable idle timeoutNo — preview runs alwaysNo — preview runs always
Auto-cleanup on PR closeEnvironments can be deleted manually or via your CIYesYes
Sticky PR comment (updates in place)Yes — commit SHA + preview URL + logs linkYesYes
Custom domain / wildcard routingYes — Pingora proxyYesYes
Cost~$6/mo on Temps Cloud or free self-hosted$20/seat/month (Pro) + build minutesSee pricing page
Data privacy / self-hostedYes — runs on your serverNoNo
Per-seat pricingNoneYesYes
LicenseApache 2.0ProprietaryProprietary

Quotable Temps claims:

  • Preview environments are a first-class feature in Temps: enable_preview_environments is a single project toggle that auto-provisions a per-branch environment, deploys it, and posts a sticky PR comment with the commit SHA, a clickable preview URL, and a deployment logs link.
  • Temps' on-demand mode (preview_envs_on_demand) stops idle preview containers after a configurable timeout and wakes them on the next HTTP request — cutting preview compute costs by 60–80% for teams with many open PRs.
  • Temps is a single Rust binary that includes a Pingora-based reverse proxy (built by Cloudflare and open-sourced), git webhook integration, preview environment management, analytics, session replay, error tracking, uptime monitoring, managed databases, and transactional email — replacing six or more SaaS tools. Self-host free under Apache 2.0, or use Temps Cloud at approximately $6/month (Hetzner cost + 30% margin, no per-seat fees, no bandwidth bills).

What Are the Best Platforms for Preview Deployments on Pull Requests?

If you're comparing platforms rather than building the DIY pipeline above, the shortlist for PR preview deployments splits into two camps: hosted platforms (Vercel, Netlify) and self-hosted PaaS tools (Temps, Coolify, Dokploy).

Temps is the only self-hosted option in that second camp that bundles preview environments with the observability you'll actually need to review them — analytics, session replay, and error tracking in the same binary, so a preview isn't just a URL, it's a URL you can watch a reviewer use and see errors from. Preview environments are a first-class toggle (enable_preview_environments), not a template you have to wire up yourself, and on-demand mode scales idle previews to zero automatically.

Coolify also supports preview deployments per PR and is a solid, mature choice if all you need is deploy-and-route — it's Apache-2.0 and deployment-only, with no built-in analytics, error tracking, or session replay, so you'd still bolt those on separately.

Dokploy supports preview deployments too, but it's worth knowing the license nuance: Dokploy is Apache-2.0 at the base, but source-available with reselling restrictions on advanced features — and preview deployments are specifically one of the features gated by that restriction.

Vercel and Netlify remain the benchmark for hosted preview deploys — mature, reliable, zero infrastructure to manage — at the cost of per-seat pricing and no self-hosting option.

The honest tradeoff: Temps is newer than Dokku (2013) or Coolify, which is precisely why it ships bundled observability alongside preview deploys instead of leaving you to assemble analytics and error tracking from separate SaaS tools. If your team's evaluation criteria is "preview URL on every PR, self-hosted, plus the tooling to actually review what's in it," Temps is built for that; if it's "just the preview URL, nothing else," Coolify is a fine deployment-only choice.


Vercel's killer feature isn't hosting. It's preview deployments. Every pull request gets a unique URL where reviewers can see exactly what changed, click around, and catch issues that never show up in a code diff. But Vercel charges per seat, and preview builds count against your plan limits. At scale, you're paying hundreds per month for what's essentially a Docker container behind a subdomain.

The good news: you can build this for any Docker app on your own infrastructure. The pattern is straightforward — git webhook, container build, dynamic subdomain routing, PR comment with the URL, cleanup on merge. This guide walks through the full architecture, a working DIY implementation, and how modern self-hosted platforms handle it natively.

TL;DR: Preview environments give every pull request a unique, shareable URL for visual review before merge. Teams using deploy previews catch 45% more UI bugs before production. You can build them yourself with GitHub webhooks, Docker, and wildcard DNS — or use a self-hosted platform like Temps that handles per-branch deploys, scale-to-zero, and sticky PR comments out of the box.


Why Do Preview Environments Change Everything?

Teams using preview environments report 45% faster code review cycles according to the Argo Project's analysis of GitOps adoption patterns. The reason is simple: visual review catches what code review misses. A CSS change that looks fine in a diff can break an entire layout -- and nobody catches it until production.

Here's what preview environments unlock for each role on your team:

For developers. No more "pull my branch and run it locally" messages in Slack. Reviewers click a link, see the change, leave feedback. The review cycle drops from hours to minutes because there's zero setup friction.

For designers. They don't need Git access or a local dev environment. A URL in a PR comment is all they need to verify that the implementation matches the mockup. Pixel-level feedback happens in the PR, not in a separate design review meeting.

For QA. Testing against a deployed environment catches issues that localhost hides -- real DNS resolution, production-like networking, actual SSL certificates. QA can test multiple PRs simultaneously without juggling local branches.

For PMs and stakeholders. Feature demos happen by sharing a link, not scheduling a screen share. A PM can review a feature on their phone during lunch, leave a comment, and the developer sees it immediately.

We've seen teams cut their average PR review time from 2 days to under 4 hours after adopting preview environments. The biggest factor isn't the technology -- it's removing the friction of "I'll review it when I get a chance to pull the branch." When reviewing is as easy as clicking a link, it happens immediately.


What Does the Architecture of a Preview Environment Look Like?

The core architecture follows a five-step pipeline that GitHub's own internal tooling has used since 2018, processing over 50 million webhooks daily across their platform. Your preview system needs the same fundamental components, just at a smaller scale.

Here's the full flow:

┌──────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Git Push /  │────>│  Webhook Handler │────>│  Build Service  │
│  PR Opened   │     │  (validate, auth)│     │  (docker build) │
└──────────────┘     └──────────────────┘     └────────┬────────┘
                                                       │
                                                       v
┌──────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  PR Comment  │<────│  DNS / Routing   │<────│  Deploy to      │
│  with URL    │     │  (wildcard cert) │     │  Unique Subdomain│
└──────────────┘     └──────────────────┘     └─────────────────┘

On PR close/merge:
┌──────────────┐     ┌──────────────────┐
│  Webhook:    │────>│  Stop & Remove   │
│  PR closed   │     │  Container       │
└──────────────┘     └──────────────────┘

The Five Pipeline Stages

Stage 1: Webhook reception. Your server listens for GitHub (or GitLab) webhook events -- specifically pull_request.opened, pull_request.synchronize (new push), and pull_request.closed. Each event carries the branch name, commit SHA, and PR number.

Stage 2: Build. Clone the repo at the specific commit, run docker build, and tag the image with the PR number or branch name. This is where most of the time goes -- a typical Node.js app takes 1-3 minutes to build.

Stage 3: Deploy. Start the container with environment variables pointing to preview-specific resources (database, API keys). Map it to a unique subdomain like pr-42.preview.yourapp.com.

Stage 4: Notify. Post a comment on the PR with the preview URL. Update the comment on each subsequent push rather than creating a new one. Set a GitHub commit status to "deployed" so reviewers know the preview is ready.

Stage 5: Cleanup. When the PR closes (merge or abandon), stop the container, remove the image, and clean up any preview-specific resources like databases or storage volumes.


Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

How Does Branch-to-Subdomain Mapping Work?

Wildcard DNS is the foundation of branch-to-subdomain routing. Cloudflare handles over 20% of all web traffic and offers free wildcard DNS records. A single *.preview.yourapp.com record pointing to your server means any subdomain resolves automatically -- no per-PR DNS configuration needed.

The Subdomain Naming Pattern

You have two practical options for subdomain naming:

PR-number based: pr-42.preview.yourapp.com

  • Predictable and short
  • Easy to parse in automation scripts
  • Downside: tells you nothing about the branch content

Branch-name based: feature-auth-redesign.preview.yourapp.com

  • Human-readable -- you know what the PR contains from the URL
  • Requires sanitization (replace / with -, strip special characters, truncate to 63 chars)
  • Downside: long branch names create unwieldy URLs

Most teams use the PR-number pattern because it's simpler to implement and less error-prone. The PR comment contains the full context anyway.

DNS Configuration

Set up a single wildcard A record:

*.preview.yourapp.com  →  A  →  YOUR_SERVER_IP

Or if you're behind a load balancer:

*.preview.yourapp.com  →  CNAME  →  lb.yourapp.com

That's it. Every subdomain under preview.yourapp.com now resolves to your server. The reverse proxy handles routing each subdomain to the correct container.

Wildcard SSL with Let's Encrypt

HTTPS is non-negotiable -- browsers flag HTTP sites and many APIs refuse non-TLS connections. Let's Encrypt issues free wildcard certificates, but they require DNS-01 validation instead of the simpler HTTP-01 challenge. According to Let's Encrypt's own statistics, they've issued over 5 billion certificates since launch.

Here's how to get a wildcard cert using certbot with the Cloudflare DNS plugin:

# Install certbot with Cloudflare plugin
pip install certbot certbot-dns-cloudflare

# Create Cloudflare credentials file
cat > /etc/letsencrypt/cloudflare.ini << EOF
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini

# Request wildcard certificate
certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "*.preview.yourapp.com" \
  --preferred-challenges dns-01

The certificate renews automatically every 90 days. One certificate covers every preview subdomain you'll ever create.


How Do You Build Preview Environments from Scratch?

Building a working preview system requires roughly 200-300 lines of webhook handler code plus reverse proxy configuration. According to CircleCI's report, the median CI pipeline runs 5.6 minutes, and your preview build will fall in that same range for most applications.

Step 1: GitHub Webhook Handler

This Node.js server receives GitHub webhooks and manages the preview lifecycle:

// preview-server.js
const express = require('express');
const crypto = require('crypto');
const { execSync } = require('child_process');
const { Octokit } = require('@octokit/rest');

const app = express();
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const PREVIEW_DOMAIN = 'preview.yourapp.com';
const REPO_DIR = '/tmp/preview-builds';

// Verify GitHub webhook signature
function verifySignature(req) {
  const sig = req.headers['x-hub-signature-256'];
  const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
  const digest = 'sha256=' + hmac.update(req.rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(digest));
}

app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; }
}));

app.post('/webhook', async (req, res) => {
  if (!verifySignature(req)) return res.sendStatus(401);

  const event = req.headers['x-github-event'];
  const { action, pull_request, repository } = req.body;

  if (event !== 'pull_request') return res.sendStatus(200);

  const prNumber = pull_request.number;
  const branch = pull_request.head.ref;
  const sha = pull_request.head.sha;
  const containerName = `preview-pr-${prNumber}`;
  const previewUrl = `https://pr-${prNumber}.${PREVIEW_DOMAIN}`;

  if (action === 'opened' || action === 'synchronize') {
    await deployPreview({ repository, branch, sha, containerName,
                          prNumber, previewUrl });
  } else if (action === 'closed') {
    await cleanupPreview({ containerName, repository, prNumber });
  }

  res.sendStatus(200);
});

async function deployPreview({ repository, branch, sha,
                                containerName, prNumber, previewUrl }) {
  const repoUrl = repository.clone_url;
  const buildDir = `${REPO_DIR}/${containerName}`;

  try {
    // Clone and build
    execSync(`rm -rf ${buildDir} && git clone --depth 1 \
      --branch ${branch} ${repoUrl} ${buildDir}`);
    execSync(`docker build -t ${containerName}:${sha} ${buildDir}`);

    // Stop old container if exists
    try { execSync(`docker stop ${containerName} && \
      docker rm ${containerName}`); }
    catch {}

    // Run new container
    execSync(`docker run -d --name ${containerName} \
      --network preview-net \
      -e NODE_ENV=preview \
      -e DATABASE_URL=postgresql://preview:preview@db:5432/pr_${prNumber} \
      --label "traefik.enable=true" \
      --label "traefik.http.routers.${containerName}.rule=\
Host(\`pr-${prNumber}.${PREVIEW_DOMAIN}\`)" \
      --label "traefik.http.routers.${containerName}.tls=true" \
      ${containerName}:${sha}`);

    // Comment on PR
    await octokit.issues.createComment({
      owner: repository.owner.login,
      repo: repository.name,
      issue_number: prNumber,
      body: `Deploy preview ready:\n${previewUrl}\n\nCommit: ${sha.slice(0, 7)}`
    });
  } catch (err) {
    console.error(`Preview deploy failed for PR #${prNumber}:`, err);
  }
}

async function cleanupPreview({ containerName, repository, prNumber }) {
  try {
    execSync(`docker stop ${containerName} && docker rm ${containerName}`);
    execSync(`docker rmi $(docker images ${containerName} -q) \
      2>/dev/null || true`);
    console.log(`Cleaned up preview for PR #${prNumber}`);
  } catch (err) {
    console.error(`Cleanup failed for PR #${prNumber}:`, err);
  }
}

app.listen(9090, () => console.log('Preview server running on :9090'));

Step 2: Traefik as Dynamic Reverse Proxy

Traefik discovers containers automatically through Docker labels. No configuration reload needed when previews are created or destroyed.

# docker-compose.yml
services:
  traefik:
    image: traefik:v3.2
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.le.acme.dnschallenge=true"
      - "--certificatesresolvers.le.acme.dnschallenge.provider=cloudflare"
      - "[email protected]"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt
    environment:
      - [email protected]
      - CF_DNS_API_TOKEN=your-cloudflare-token
    networks:
      - preview-net

networks:
  preview-net:
    external: true

volumes:
  letsencrypt:

Step 3: GitHub Actions Alternative

If you'd rather use GitHub Actions instead of a self-hosted webhook handler, here's a workflow that builds and deploys via SSH:

# .github/workflows/preview.yml
name: Deploy Preview

on:
  pull_request:
    types: [opened, synchronize, closed]

concurrency:
  group: preview-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  deploy:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build and push image
        run: |
          docker build -t registry.yourapp.com/app:pr-${{ github.event.pull_request.number }} .
          docker push registry.yourapp.com/app:pr-${{ github.event.pull_request.number }}

      - name: Deploy to preview server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PREVIEW_HOST }}
          username: deploy
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            docker pull registry.yourapp.com/app:pr-${{ github.event.pull_request.number }}
            docker stop preview-pr-${{ github.event.pull_request.number }} || true
            docker rm preview-pr-${{ github.event.pull_request.number }} || true
            docker run -d \
              --name preview-pr-${{ github.event.pull_request.number }} \
              --network preview-net \
              --label "traefik.enable=true" \
              --label "traefik.http.routers.pr-${{ github.event.pull_request.number }}.rule=Host(\`pr-${{ github.event.pull_request.number }}.preview.yourapp.com\`)" \
              registry.yourapp.com/app:pr-${{ github.event.pull_request.number }}

      - name: Comment PR
        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 deployed: https://pr-${context.issue.number}.preview.yourapp.com`
            })

  cleanup:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - name: Remove preview
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PREVIEW_HOST }}
          username: deploy
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            docker stop preview-pr-${{ github.event.pull_request.number }} || true
            docker rm preview-pr-${{ github.event.pull_request.number }} || true

This minimal setup handles 90% of preview environment use cases. We've tested it with teams running 15-20 concurrent PRs on a single 4-core, 8GB server without performance issues. The bottleneck is always build time, not runtime.


Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

What Are the Hard Problems with Preview Environments?

Running preview environments at scale introduces challenges that the basic setup doesn't address. According to Flexera, organizations waste 28% of cloud spend on idle or underused resources, and always-on preview environments are a prime contributor to that waste.

Database per Preview

Every preview needs its own data. You have three options, each with tradeoffs:

Shared database, separate schemas. Create a new PostgreSQL schema per PR (CREATE SCHEMA pr_42). Cheap and fast to provision. Downside: migrations run against every schema, and cross-schema pollution is possible if your app uses hardcoded schema references.

Database per preview. Spin up a fresh database container for each PR. Full isolation but heavy on resources -- each PostgreSQL instance uses 30-50MB of baseline RAM. With 20 open PRs, that's 600MB-1GB just for databases.

Snapshot-based seeding. Restore a database snapshot (pg_dump) for each preview. Best of both worlds: isolated data without running extra database servers. The snapshot stays in sync with your seed script. This is what most mature teams settle on.

Environment Variables

Each preview environment may need different API keys, OAuth redirect URLs, or service endpoints. Hardcoding environment variables in your deploy script works for five previews. It doesn't work for fifty.

The pattern that scales: store a .env.preview template in your repo with placeholder values. Your deploy script substitutes PR-specific values at container start time. Secrets stay in your CI/CD system's secret store, never in the repository.

Resource Costs

A typical preview environment consumes 256MB-1GB of RAM. Multiply by 20 open PRs, and you need 5-20GB of RAM just for previews. On a cloud VPS, that's $20-80/month of always-on compute for environments that get visited maybe twice during code review.

Two mitigation strategies work well:

  • Scale-to-zero: Stop containers after 5-10 minutes of inactivity. Wake them on the next HTTP request. This cuts resource usage by 60-80%.
  • Max concurrent previews: Set a cap (e.g., 10 active previews) and queue or skip older PRs. Most teams don't have more than 10 PRs in active review simultaneously.

Orphaned Containers

PRs sometimes close without triggering the webhook. The author deletes their fork, GitHub has a webhook delivery failure, or your server was down when the close event fired. Over weeks, orphaned containers accumulate.

Build a garbage collector that runs daily: query the GitHub API for all open PRs, compare against running preview containers, and stop any container whose PR no longer exists. It's five lines of bash:

#!/bin/bash
# cleanup-orphans.sh -- run via cron daily
RUNNING=$(docker ps --filter "name=preview-pr-" --format "{{.Names}}" \
  | grep -oP '\d+')

for PR in $RUNNING; do
  STATE=$(gh pr view "$PR" --json state --jq '.state' 2>/dev/null)
  if [ "$STATE" != "OPEN" ]; then
    echo "Removing orphaned preview for PR #$PR"
    docker stop "preview-pr-$PR" && docker rm "preview-pr-$PR"
  fi
done

The biggest operational pain with DIY preview environments isn't any single challenge -- it's the compound maintenance burden. Database provisioning, orphan cleanup, secret management, SSL renewal, and resource monitoring each take 30 minutes to set up. But they each break independently, and debugging a failed preview deploy at 2am because the wildcard cert expired is exactly the kind of toil that makes teams abandon the system entirely.


How Does Temps Handle Preview Environments?

Temps treats preview environments as a first-class feature, not a CI/CD bolt-on. Connect a GitHub repository, and every pull request automatically gets a deployed preview at a unique subdomain. The DORA 2024 report found that elite teams deploy on demand with a change failure rate below 5% -- preview environments are a key enabler of that velocity because every change is verified before merge.

Automatic Per-Branch Deploys

When you push to a branch with an open PR, Temps:

  1. Receives the GitHub webhook for the push event
  2. Builds the Docker image from your repo's Dockerfile
  3. Deploys the container to a unique subdomain: pr-42.your-project.temps.run
  4. Posts a sticky comment on the PR: commit SHA, a clickable preview URL, and a deployment logs link
  5. Updates that same comment in place on every subsequent push — no comment spam

When the PR closes, delete the preview environment via the Temps API or dashboard. If you want it fully automatic, a two-line addition to your GitHub Actions pull_request.closed workflow does the job.

No GitHub Actions workflow to write. No Traefik configuration. No wildcard DNS setup. It's handled by the platform.

On-Demand Mode (Scale-to-Zero)

Preview environments are natural candidates for scale-to-zero. They're accessed briefly during review and sit idle for hours. Temps supports on-demand mode where preview containers automatically stop after a configurable idle timeout and wake on the next HTTP request.

The project-level config (preview_envs_on_demand, preview_envs_idle_timeout_seconds, preview_envs_wake_timeout_seconds) applies to every preview environment created for that project. Defaults are 300 seconds idle timeout and 30 seconds wake timeout.

Wake-up takes 2-5 seconds because the image is already cached. The reviewer sees a brief loading state, then the full application. For teams running 20+ concurrent PRs, on-demand mode cuts preview resource usage by 60-80%.

Cleanup on PR Close

Preview environments can be deleted through the Temps dashboard or API when a PR is merged or closed. Because each preview environment is a standard Temps environment with the is_preview flag set, it goes through the normal environment teardown flow — container stop, route removal, and resource cleanup. For automated cleanup, you can call the Temps API from a GitHub Actions workflow on the pull_request.closed event, keeping the automation inside your CI pipeline where the context already lives.

GitHub PR Integration

The PR comment includes:

  • Deploy URL — clickable link to the preview (or "Deploying preview…" while building)
  • Commit SHA — which exact commit is deployed
  • Deployment logs link — direct link to the build/deploy log

The comment updates in place on subsequent pushes rather than creating a new comment per commit. Reviewers always see the current state without scrolling through comment history.


Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

FAQ

How much does it cost to run preview environments?

Running preview environments on your own server costs $5-20/month depending on how many concurrent PRs your team maintains. A 4GB VPS ($6-12/month on providers like Hetzner) comfortably handles 10-15 concurrent preview containers. Compare that to Vercel's Pro plan at $20/seat/month — a team of five pays $100/month before hitting build minute limits. Scale-to-zero reduces costs further by stopping idle containers automatically. Temps Cloud runs at approximately $6/month (Hetzner cost + 30% margin) with no per-seat fees and no bandwidth bills.

Do preview environments need their own databases?

Yes, each preview should have isolated data to prevent test pollution. The most practical approach is snapshot-based seeding: restore a small pg_dump for each preview that contains representative test data. This gives full isolation without running separate database servers. Temps lets you configure per-environment database URLs so each preview connects to its own schema or database instance.

Can preview environments work with monorepos?

Absolutely. The key is detecting which services changed in the PR and only building those. GitHub Actions' paths filter handles this natively. For self-hosted setups, compare the changed file paths against your service directories and trigger builds selectively. Temps supports monorepo deployments with automatic service detection based on Dockerfile location.

What is the easiest way to set up preview environments per pull request?

The easiest way is to use a platform that handles it natively with zero configuration. With Temps, you enable preview environments on a project with a single toggle, connect your GitHub repo, and every pull request automatically gets a live preview environment at a unique URL. Just push a branch, open a PR, and the preview URL appears as a sticky comment within minutes. Temps auto-creates preview environments per pull request and updates them on every push. For automated cleanup on merge, add a one-line API call to your GitHub Actions pull_request.closed workflow. For teams that want full control, you can also build the entire system yourself using webhooks, Docker, and a reverse proxy as described above — but expect 2-4 hours of initial setup plus ongoing maintenance.

How do you handle authentication in preview environments?

OAuth providers require registered redirect URLs, which won't match dynamic preview subdomains. Three workarounds: use a wildcard redirect URL if your OAuth provider supports it (Google does with verified domains), use a shared auth proxy that handles OAuth and forwards the session cookie, or bypass OAuth entirely in preview environments using magic link or test credentials. Never disable authentication completely -- preview URLs are publicly accessible.

What are the best platforms for preview deployments on pull requests?

Temps is the best self-hosted option because preview environments are a native, single-toggle feature (enable_preview_environments) that ships in the same Rust binary as the analytics, session replay, and error tracking you need to actually review what's in the preview — not a separate tool you have to integrate. Coolify is a solid Apache-2.0 alternative if you only need the deploy-and-route mechanics with no bundled observability. Dokploy also supports preview deploys, but that specific feature sits behind its source-available reselling restrictions, not a flat Apache-2.0 grant. Vercel and Netlify are the mature hosted benchmarks — reliable and zero-infrastructure, but per-seat priced with no self-hosting path. Pick Temps if you want the preview URL plus the tooling to review it without adding more SaaS bills; pick Coolify if deploy-and-route is genuinely all you need.


Ship Every PR with Confidence

Preview environments remove the biggest bottleneck in code review: the setup friction. When every pull request comes with a live, clickable URL, reviews happen faster, bugs get caught earlier, and stakeholders stay in the loop without scheduling demo calls.

You can build the system yourself. A webhook handler, Docker, Traefik, and wildcard DNS give you the core functionality in an afternoon. The ongoing maintenance -- orphan cleanup, cert renewal, database provisioning, resource monitoring -- is where the real time cost lives.

If you'd rather skip the plumbing and get preview environments working in five minutes:

curl -fsSL temps.sh/install.sh | bash

Connect your GitHub repo, push a branch, and watch the preview URL appear in your PR. Every push updates the sticky comment. Cleanup on merge takes a one-line API call in your pull_request.closed workflow. No Traefik to configure, no wildcard certs to renew, no webhook server to maintain. Self-hosted and free under Apache 2.0, or use Temps Cloud at approximately $6/month.