Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Preview Environments for Every PR

By Kokil Thapa | Last reviewed: September 2026

Preview Environments for Every PR give reviewers a live, isolated copy of your application before merge. A reviewer clicks a link on the pull request and sees the exact branch running with its migrations, assets, and config applied. That beats screenshots and long staging deploy threads. On production Laravel applications I've maintained with GitLab CI and Deployer 7, preview URLs cut review cycles from days to hours. This guide walks through architecture, CI config, Laravel-specific setup, cost control, and cleanup — the same patterns I use on sister sites sharing a Deployer pipeline.

What are Preview Environments for Every PR?

A preview environment is a short-lived deployment tied to one branch or pull request. It gets its own URL, database schema, and runtime config. Reviewers test real flows — login, checkout, file upload — without touching staging or production.

The pattern differs from a shared staging server. Staging holds one mutable copy of the app. Preview environments are disposable. Each PR gets a fresh namespace. Merge the PR and the environment disappears.

In practice, three layers make this work:

  • Provisioning — create a host, container, or namespace for the branch.
  • Deploy — build assets, run Composer, apply migrations, warm caches.
  • Feedback loop — post the preview URL as a CI comment and run automated checks against it.

If you already run staging, treat it as the baseline. Read our guide on setting up a staging environment that mirrors production before adding per-PR previews on top.

Preview Environments for Every PR — FlowOpen PRfeature branchCI Pipelinebuild + testDeployephemeral URLReviewQA + approveIsolated DBper preview schemaSmoke TestsHTTP + auth checksMerge or Close PRauto teardown + DB drop
Preview Environments for Every PR: from branch push through deploy, review, and automatic teardown after merge.

Platforms like GitLab call these Review Apps. GitHub Actions uses environment URLs. Kubernetes teams often use namespace-per-branch with Helm or Argo CD. The naming differs. The goal is the same — one live URL per PR.

How do you choose a platform for preview environments?

Pick the option your team can run and debug at 2 a.m. A fancy Kubernetes setup helps nothing if nobody understands ingress rules.

ApproachBest forTrade-offTypical cost
GitLab Review AppsTeams already on GitLab CITied to GitLab; needs a runner with deploy accessRs 3,000–8,000/mo (~USD 22–60) per small VM
GitHub Actions + SSH deployGitHub repos with existing Deployer scriptsYou write teardown logic yourselfRunner minutes + one preview host
Docker Compose on a preview hostLaravel/PHP monoliths, small teamsManual port and subdomain routingOne EC2 or VPS, Rs 5,000/mo (~USD 37)
Kubernetes namespace per PRMicroservices, high PR volumeCluster ops overhead; needs ingress + cert automationCluster baseline + per-pod CPU
PaaS (Render, Railway, Fly.io)Fast proof of conceptLess control; vendor pricing at scalePay per preview hour

For Laravel monoliths on a single Ubuntu server, Docker Compose plus Nginx virtual hosts is often enough. I've used this on legal-tech portals and booking systems where PR volume stays under twenty per week. For higher volume, read about Terraform workspaces and environments to keep infra definitions repeatable.

Official references: GitLab Review Apps documentation and GitHub Actions environment deployments.

How do you set up preview environments in GitLab CI?

GitLab Review Apps are the fastest path if you already use GitLab CI. The pipeline builds the app, deploys to a predictable subdomain, and posts the URL on the merge request.

Step 1 — Define a dynamic environment name

GitLab derives the environment from the branch slug. Keep names DNS-safe and short.

# .gitlab-ci.yml excerpt
deploy_preview:
  stage: deploy
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.preview.example.com
    on_stop: stop_preview
  script:
    - dep deploy preview-$CI_COMMIT_REF_SLUG
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Step 2 — Add a stop job for teardown

Without a stop job, preview hosts fill up fast. GitLab triggers on_stop when the MR closes or merges.

stop_preview:
  stage: deploy
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  script:
    - dep deploy preview-$CI_COMMIT_REF_SLUG --task cleanup
  when: manual
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual

Make teardown automatic in practice by calling cleanup from a webhook or scheduled job. Manual stop jobs get forgotten.

Step 3 — Post the URL back to the merge request

GitLab does this automatically when environment.url is set. Reviewers see a "View app" button on the MR widget. Pair it with AI code review in your CI pipeline so static analysis and live preview happen in one pass.

GitLab CI Preview Pipelinelinttestbuild assetsdeployEnvironment: review/feature-slughttps://feature-slug.preview.example.comMR mergedtriggers stop jobCleanupdrop DB + remove vhost
GitLab CI stages for Preview Environments for Every PR: lint, test, build, deploy, and automatic stop on merge.

How do you configure preview environments for Laravel applications?

Laravel apps need a few extra steps beyond a generic static site. You must handle .env, migrations, queued jobs, and compiled assets. PHP 8.3 or higher is required for Laravel 13; Laravel 12 runs on PHP 8.2+. Match your production PHP-FPM version on the preview host.

Environment file strategy

Never copy production secrets into previews. Generate a preview-specific .env from CI variables:

APP_ENV=preview
APP_URL=https://${BRANCH_SLUG}.preview.example.com
APP_KEY=base64:...generated per preview...
DB_DATABASE=preview_${BRANCH_SLUG}
QUEUE_CONNECTION=sync
MAIL_MAILER=log

Disable outbound mail and payment gateways in preview. Route webhooks to a mock endpoint or log driver. On a client project with eSewa integration, I pointed preview callbacks to a JSON formatter tool endpoint during QA — never to live payment APIs.

Database isolation

One schema or database per preview is the safe default. Shared preview databases cause migration conflicts when two PRs alter the same table.

  1. Create database preview_feature_slug before deploy.
  2. Run php artisan migrate --force against that database.
  3. Optionally seed with anonymised fixtures — never production dumps with PII.
  4. Drop the database in the stop job.

Read our notes on database migrations in team environments before running parallel migration paths across previews.

Asset builds and Deployer 7

My production servers often have no Node.js installed. I build frontend assets in CI with Node.js 26 LTS and commit or rsync the public/build directory to the preview release. Deployer 7 handles symlinked releases the same way as production:

# deploy.php excerpt
host('preview-' . getenv('BRANCH_SLUG'))
    ->setHostname('preview.example.com')
    ->set('deploy_path', '/var/www/previews/{{branch_slug}}')
    ->set('branch', getenv('CI_COMMIT_REF_NAME'));

After symlink swap, reload PHP-FPM so opcache picks up the new code. This is the same step that fixes stale-code bugs on production deploys. See the Laravel 12 deployment guide for optimisation commands like config:cache and route:cache.

Nginx routing for subdomains

Wildcard DNS (*.preview.example.com) plus one Nginx template covers all branches:

server {
    listen 443 ssl;
    server_name ~^(?<branch>[a-z0-9-]+)\.preview\.example\.com$;
    root /var/www/previews/$branch/current/public;
    # ... standard Laravel try_files ...
}

Issue a wildcard TLS cert with Let's Encrypt DNS validation. HTTP-only previews leak session cookies and fail mixed-content checks on asset URLs.

For local parity before pushing, use Docker Compose for local Laravel development. Match PHP 8.5 or 8.4 locally if production preview runs 8.4.

Laravel Preview StackNginx + TLS wildcardPHP-FPM 8.4 + Laravel 13MySQL 9.7preview_branch DBRedis 8.10cache + sessionsDeployer 7 symlink release per branch
Typical Laravel preview stack: wildcard Nginx, PHP-FPM, isolated MySQL schema, Redis cache, and Deployer symlink releases.

What does it cost to run preview environments for every PR?

Cost scales with PR count, preview lifetime, and resource size. A single small EC2 instance can host five to ten lightweight Laravel previews if you cap idle time.

Rule of thumb for Nepal-based teams on budget hosting:

  • Preview host: Rs 5,000–12,000/month (~USD 37–90) for a 4 GB RAM VPS.
  • Wildcard DNS + TLS: free with Cloudflare or Let's Encrypt.
  • CI minutes: included on GitLab.com free tier for small teams; self-hosted runners cost electricity and ops time.
  • Database storage: grows if previews are not torn down — budget disk alerts.

Cost controls that actually work:

  1. Auto-stop after 48 hours — cron deletes environments with no MR activity.
  2. Deploy previews only on label — add deploy:preview label to skip docs-only PRs.
  3. Shared Redis, isolated DB — one Redis 8.10 instance with key prefixes saves RAM.
  4. Skip previews for dependency-bot PRs — run tests only, no deploy.

On a Laravel + Livewire booking project I shipped, preview deploys ran only when the MR touched backend or frontend code. Documentation PRs skipped deploy and saved roughly forty percent of CI time.

For Symfony apps, similar patterns apply — see Symfony environment config for multi-environment apps for env var handling across preview and staging tiers.

How do you secure and clean up ephemeral preview environments?

Preview URLs are often guessable subdomains. Treat them as semi-public. Never expose real customer data. Never point previews at production databases.

Authentication and access

Options ranked by simplicity:

  • HTTP basic auth on the Nginx vhost — good enough for internal QA.
  • VPN or IP allowlist — restrict to office and CI runner IPs.
  • OAuth gate middleware — Laravel middleware that checks GitLab/GitHub group membership.

Disable admin panels and Horizon dashboards on previews unless behind auth. I've seen staging admin URLs indexed by Google. Add noindex headers and robots disallow on preview hosts.

Secrets and third-party APIs

Use sandbox API keys for Stripe, PayPal, and local gateways like Khalti in preview mode. Set APP_DEBUG=false even on previews — debug pages leak env keys. Rotate preview APP_KEY per environment; never reuse production keys.

Reliable teardown

Orphaned previews are the main long-term cost. Your stop job must:

  1. Remove the Deployer release directory.
  2. Drop the preview database.
  3. Delete the Nginx vhost and reload.
  4. Clear Redis keys with the branch prefix.
  5. Remove DNS record if not using wildcard.

Schedule a nightly reconciler that lists active preview directories and compares them to open MRs. Delete anything stale. Sister sites on our shared Deployer 7 pipeline use this pattern — the same hosts that run Notary Kathmandu and related legal-tech portals.

Preview Security ChecklistIsolated DB — no prod dataSandbox API keys onlyHTTP auth or IP allowlistnoindex + robots blockAPP_DEBUG=falseAuto teardown on mergeNightly orphan cleanup croncompare disk dirs vs open MRs
Security essentials for Preview Environments for Every PR: isolated data, sandbox keys, access control, and scheduled cleanup.

Automate server provisioning with Ansible playbooks for PHP server provisioning so preview hosts stay consistent. For ongoing ops, support and maintenance contracts should include preview host monitoring.

Key Takeaways

  • Preview Environments for Every PR give reviewers a live URL tied to the branch — not a shared staging slot.
  • GitLab Review Apps or GitHub Actions plus Deployer 7 cover most Laravel teams without Kubernetes.
  • Isolate databases per preview; never attach production data or live payment keys.
  • Build frontend assets in CI with Node.js 26 LTS when the server has no Node installed.
  • Auto-teardown on MR close is non-negotiable — schedule orphan cleanup as a safety net.
  • Gate deploys with labels or path filters to control cost on high-volume repos.

People Also Ask

What is the difference between staging and a preview environment?

Staging is a shared, long-lived environment that mirrors production config. A preview environment is ephemeral and tied to one PR. Staging validates release candidates. Previews validate individual feature branches before they merge.

Can you run preview environments without Kubernetes?

Yes. A single Ubuntu VPS with Nginx, PHP-FPM, MySQL, and Deployer 7 handles multiple Laravel previews via wildcard subdomains. Kubernetes helps at scale but adds operational overhead most small teams do not need.

How long should a preview environment stay alive?

Keep previews alive while the MR is open, plus a short grace period of 24–48 hours. Auto-delete after merge or close. Long-lived previews waste disk, clutter DNS, and drift from the branch tip.

Do preview environments work with WordPress or WooCommerce?

They do, but database copies are heavier. Clone a sanitised snapshot, run search-replace on URLs, and disable outbound email. WooCommerce 11.1 preview sites should use test payment gateways only. For WordPress-specific hosting, see our WordPress development service.

Ship faster with live PR previews

Preview Environments for Every PR turn code review from async guesswork into clicked-path verification. Start with one preview host, GitLab Review Apps or a GitHub Actions deploy job, isolated databases, and a teardown script you trust. Expand to Kubernetes only when PR volume demands it.

If you want this wired into your Laravel app, existing CI pipeline, or client portal project, I can help design and deploy the full stack. Explore enterprise application development and testing and optimization services, or custom software development for greenfield setups. Need a second opinion on your current pipeline? Contact us and describe your repo, host, and PR volume — we'll map the lightest path to Preview Environments for Every PR that your team can maintain.

Frequently Asked Questions

Short-lived deployments tied to one branch or pull request, each with its own URL, database schema, and runtime config. Reviewers test real flows without touching staging or production.

Staging is a shared, long-lived environment that mirrors production config and validates release candidates. A preview environment is ephemeral and tied to one PR. Each merge request gets a fresh, disposable namespace with its own URL. Merge the PR and the preview disappears. Staging holds one mutable copy; previews isolate every feature branch before it merges into main.

Yes. A single Ubuntu VPS with Nginx, PHP-FPM, MySQL, and Deployer 7 handles multiple Laravel previews via wildcard subdomains without any cluster.

Budget Rs 5,000–12,000/month (~USD 37–90) for a 4 GB preview VPS. Wildcard TLS is free via Let's Encrypt or Cloudflare.

Use GitLab Review Apps with a deploy_preview job that sets environment name to review/$CI_COMMIT_REF_SLUG and url to your wildcard subdomain. Add rules so deploy runs only on merge_request_event. Define a stop_preview job with action stop and on_stop pointing to it so GitLab tears down when the MR closes or merges. GitLab posts a View app button automatically when environment.url is set. Make stop automatic in practice — manual stop jobs get forgotten.

Generate a preview-specific .env from CI variables: APP_ENV=preview, isolated DB_DATABASE=preview_${BRANCH_SLUG}, QUEUE_CONNECTION=sync, and MAIL_MAILER=log. Run php artisan migrate --force against one schema per preview. Build frontend assets in CI with Node.js 26 LTS when the server has no Node installed, then rsync public/build via Deployer 7. Match production PHP-FPM version — PHP 8.3 or higher for Laravel 13, PHP 8.2+ for Laravel 12. Reload PHP-FPM after symlink swap so opcache picks up new code.

Pick what your team can run and debug at 2 a.m. GitLab Review Apps suit teams already on GitLab CI. GitHub Actions plus SSH deploy works if you already use Deployer scripts but you write teardown yourself. Docker Compose on one preview host fits Laravel monoliths with low PR volume. Kubernetes namespace-per-PR helps microservices at high volume but adds ingress and cert ops. PaaS options like Render, Railway, or Fly.io are fast proofs of concept with less control at scale.

Keep previews alive while the merge request is open, plus a short grace period of 24–48 hours after close or merge. Auto-delete beyond that. Long-lived previews waste disk, clutter DNS if you are not using wildcard routing, and drift from the branch tip as other PRs merge. Schedule a nightly reconciler as a safety net to catch orphaned environments that outlive their MR.

Treat preview URLs as semi-public because subdomains are often guessable. Never attach production databases or real customer data. Use HTTP basic auth on the Nginx vhost, VPN or IP allowlists, or Laravel middleware checking GitLab or GitHub group membership. Set APP_DEBUG=false, rotate a unique APP_KEY per preview, and use sandbox keys for Stripe, PayPal, Khalti, and eSewa. Add noindex headers and disable admin panels unless behind auth. Route webhooks to mock endpoints during QA.

Your stop job must remove the Deployer release directory, drop the preview database, delete the Nginx vhost and reload, clear Redis keys with the branch prefix, and remove DNS records if not using wildcard. GitLab triggers on_stop when the MR closes. Because manual stop jobs get forgotten, automate teardown via webhooks or scheduled jobs. Run a nightly reconciler that lists active preview directories, compares them to open MRs, and deletes anything stale.

They do, but database copies are heavier than a typical Laravel preview. Clone a sanitised snapshot, run search-replace on URLs, and disable outbound email. WooCommerce 11.1 preview sites must use test payment gateways only — never live checkout keys. The same isolation principles apply: one environment per PR, sandbox credentials, and teardown after merge. For teams already on WordPress hosting workflows, the provisioning layer differs but the review pattern is identical.

One schema or database per preview is the safe default. Shared preview databases cause migration conflicts when two PRs alter the same table simultaneously. Create preview_feature_slug before deploy, run migrations against it, optionally seed with anonymised fixtures, and drop the database in the stop job. Never use production dumps containing PII. A shared Redis 8.10 instance with key prefixes per branch saves RAM while keeping database isolation strict.

GitLab Review Apps are built into GitLab CI and post URLs on merge requests automatically. GitHub Actions supports environment URLs with deploy jobs you configure yourself, including teardown logic. Kubernetes teams use namespace-per-branch with Helm or Argo CD. All three follow the same pattern: provision, deploy, post the URL, run checks, tear down on merge. Pick GitLab Review Apps for the fastest path if you already run GitLab CI pipelines.

Auto-stop after 48 hours of MR inactivity. Gate deploys behind a deploy:preview label so documentation-only PRs skip provisioning. Skip previews for dependency-bot PRs and run tests only. One small EC2 instance can host five to ten lightweight Laravel previews if you cap idle time. Use shared Redis with isolated databases. On a Laravel and Livewire booking project, deploying only when MRs touched backend or frontend code saved roughly forty percent of CI time.

Use wildcard DNS pointing *.preview.example.com to your preview host, then one Nginx template with a regex server_name capturing the branch slug. Each branch maps to its Deployer symlink release at /var/www/previews/$branch/current/public. Issue a wildcard TLS certificate with Let's Encrypt DNS validation — HTTP-only previews leak session cookies and fail mixed-content checks on asset URLs. This pattern handles multiple concurrent PRs on a single Ubuntu VPS without Kubernetes.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: