Migrate to RunxBuild today!! Deploy More while Spending Less

Calculate your savings
unxBuild
Back to Blog Explainer

Deploy Previews: What They Are, How They Work, and the Gotchas

Sean

Platform Writer

Sep 23, 2026
8 min read

A deploy preview is a temporary, fully built version of an app created for a single pull request and published at its own URL, so a reviewer can click through the actual change instead of reading a diff and imagining it. Platforms like Netlify, Vercel, and Cloudflare Pages build one automatically on every PR: the CI system runs the build, publishes the output to a unique subdomain, and posts the link back to the pull request as a status check. The build disappears, or should, once the PR merges or closes. Done well, a deploy preview catches visual regressions and broken flows before a single reviewer eyeballs code. Done carelessly, it leaks secrets to forked PRs, gets indexed by search engines half-finished, and quietly doubles a team’s monthly build minutes.

Deploy Previews: What They Are, How They Work, and the Gotchas

This covers what a deploy preview actually is under the hood, how the major platforms wire it up, the failure modes worth planning for before the first awkward screenshot lands in a team channel, and a working GitHub Actions recipe for a host that does not build previews automatically.

Table of contents

How a deploy preview actually gets built

The trigger is almost always a pull request event in CI: opened, reopened, or a new commit pushed to an open PR. That event runs the normal build — install dependencies, run the build command, produce the output directory — and then publishes that output somewhere reachable by URL instead of, or in addition to, wherever production lives. Each platform names and routes that URL a little differently.

  • Netlify builds a Deploy Preview per PR and comments the link on the pull request as a commit status.
  • Cloudflare Pages generates a preview at a randomly-hashed subdomain per branch, updated on every push.
  • A plain static host can do the same thing with a purpose-built GitHub Action that publishes the build to a path like pr-preview/pr-42/ and comments the link, tearing the directory down when the PR closes.

The mechanism is the same everywhere: build on the PR event, publish somewhere addressable, and clean up on merge or close. A CI/CD pipeline that already runs tests on a pull request is most of the way there — see the overview in CI/CD pipeline tools for how that trigger is usually wired.

Environment variables and secrets on a PR build

A deploy preview still needs environment variables to build correctly, and that is exactly where the risk shows up. A pull request from a fork should never receive the same secrets as a push to the main branch, because a fork’s workflow file is attacker-controlled the moment someone opens a PR with a modified CI script. GitHub Actions draws this line with two different trigger events: pull_request, which runs with no access to repository secrets when the PR is from a fork, and pull_request_target, which runs with full secret access regardless of where the PR came from and checks out the base branch’s workflow, not the fork’s.

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

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      # no secrets referenced here -- forked PRs get a build with none

If a preview genuinely needs an API key to render correctly, use a read-only, low-privilege key scoped to non-production data rather than the same key production uses. The workflow permissions covered in GitHub Actions across repositories apply directly here.

The shared database problem

Plenty of preview setups point every open PR’s build at the same staging database, which works fine until two PRs are open at once and one of them adds a NOT NULL column, renames a table, or seeds data the other PR’s tests do not expect. A preview that talks to a shared, mutable database is not actually isolated — it just looks isolated because the frontend URL is unique.

The fixes scale with how much the app needs a database at all. A static marketing site or docs preview usually needs nothing more than the build output, so this never comes up. An app with a real backend either needs a disposable database per preview seeded from a fixture, a shared read-only replica for anything that only reads, or an accepted rule that database-touching PRs get manually coordinated. None of these are exotic; the mistake is not deciding at all and discovering the collision when two previews start behaving strangely at the same time.

Preview URLs and search engines

A preview build is, by construction, an exact copy of the real site published at a real, crawlable URL. If nothing stops it, that URL gets found — through a linked comment, a shared Slack message, or a crawler that walks outbound links — and indexed as duplicate content, sometimes outranking the production page for an unlucky query if the preview happens to load faster or the production page was briefly down.

The fix is small and easy to skip: send an X-Robots-Tag: noindex header or a <meta name="robots" content="noindex"> tag on every preview response, and keep preview subdomains out of any sitemap. Cloudflare Pages does this by default on its .pages.dev preview URLs. A DIY setup built on GitHub Actions has to add the header itself, either at the CDN or by injecting the meta tag into the build for preview deploys specifically.

What this actually costs

Building the entire site on every commit to every open pull request is not free. A team with a dozen open PRs, each getting rebuilt on every push, can burn through CI minutes faster than the same team’s actual production deploys, especially once a build takes a few minutes and commits arrive in a steady stream during active review.

The usual mitigations are a debounce on rapid pushes so only the latest commit in a short window actually triggers a build, skipping previews entirely for docs-only or dependency-lockfile-only changes, and deleting a preview’s artifacts as soon as the PR closes rather than letting them accumulate in storage. None of this is complicated, but it has to be a deliberate setting rather than the default “build everything, always” that most starter workflows ship with.

DIY: deploy PR previews with plain GitHub Actions

A host without native preview support can still get most of the benefit with a workflow that builds on pull_request, publishes the output to a path scoped to the PR number, comments the link back, and tears the deployment down on closed. The shape is consistent enough that it is worth wiring once and reusing across projects.

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

jobs:
  preview:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - name: publish preview
        run: echo "publish ./dist to pr-preview/pr-${{ github.event.number }}/"
      - name: comment link
        run: echo "comment PR with preview URL"

  teardown:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - name: remove preview
        run: echo "delete pr-preview/pr-${{ github.event.number }}/"

The two jobs — build-and-publish, and teardown-on-close — are the whole pattern. Everything else in this post (secrets scoping, noindex headers, database isolation) is a refinement layered on top of that base.

How this fits the rest of the stack

Deploy previews are a genuinely good habit even without a platform that automates them: build the pull request, look at it, then merge. What RunxBuild gives you today is the other half of that discipline once code lands on a branch that actually deploys — a build log for every deploy, deploy history you can scroll back through, and a one-click rollback to the last version that worked when a change to production turns out to have been the mistake. If you are weighing where the next service or static site should run once the preview habit is sorted, the RunxBuild hosting calculator lays out what a service, a database, and static bandwidth cost on their own lines.

Useful related references:

FAQ

What is a deploy preview?

A deploy preview is a temporary, fully built version of a pull request’s changes, published at its own URL so a reviewer can click through the real result instead of reading a diff. Most Git-connected hosting platforms build one automatically on every pull request and remove it once the PR merges or closes.

Do deploy previews cost money?

Yes, indirectly. Each preview is a full build, so a team with many open pull requests that each rebuild on every push can consume more CI minutes and storage than their actual production deploys use in the same period. Debouncing rapid pushes, skipping previews for docs-only changes, and deleting artifacts once a PR closes keeps that cost predictable rather than growing quietly with every open branch.

How do I stop deploy previews from being indexed by Google?

Send an X-Robots-Tag: noindex header, or a matching meta tag, on every response from a preview URL, and keep preview subdomains out of any sitemap. Some platforms do this automatically on their preview domains. A DIY setup built on plain GitHub Actions needs to add the header explicitly, either at the CDN layer or injected into the build only for preview deploys specifically.

Should a deploy preview share a database with production?

Not without thinking it through first. A shared, mutable staging database lets one open pull request’s schema change or seed data quietly break another PR’s preview at the same time. A disposable per-preview database seeded from a fixture, a shared read-only replica for anything that only reads, or manual coordination on database-touching PRs are the usual fixes teams settle on.

Can I build my own deploy previews with GitHub Actions?

Yes. A workflow triggered on the pull_request event builds the app, publishes the output to a path scoped to the PR number, and comments the link back on the pull request, while a second job triggered on the closed event removes that deployment. That two-job pattern covers most of what a platform’s built-in preview feature does automatically.

#deploy preview#pr preview url#preview deployment#preview environments#github actions preview