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.

Feature Branch Deployment Workflow

By Kokil Thapa | Last reviewed: August 2026

A broken feature branch deployment workflow is the single fastest way to stall momentum on a multi-developer Laravel project. When testers cannot verify changes in an isolated environment that mirrors production, bugs slip into main and hotfixes become chaotic. For teams building complex systems like legal-tech portals or eCommerce platforms, you need a reliable mechanism to spin up ephemeral preview environments automatically via CI/CD pipeline automation before merging code.

How does a feature branch deployment workflow prevent staging pollution?

The core problem with traditional "single staging server" workflows is state collision. If Developer A pushes a migration adding a column, and Developer B pushes code removing it, whoever deploys last breaks the other’s test session. In my experience maintaining multiple sister sites on shared EC2 infrastructure, this friction kills velocity. A proper feature branch deployment workflow solves this through isolation at three levels: filesystem, database, and configuration.

Isolation Architecture per BranchBranch: feat/paymentsSubdomain: feat-payments.app.npDB: app_feat_paymentsCache Prefix: feat_payments_Status: Active PreviewBranch: fix/invoice-bugSubdomain: fix-invoice.app.npDB: app_fix_invoiceCache Prefix: fix_invoice_Status: Active PreviewBranch: main (Production)Domain: app.npDB: app_productionCache Prefix: prod_Status: Live TrafficShared Server Resources (Ubuntu 24.04 + PHP-FPM 8.4)Single Nginx Config • Shared Redis Instance • Centralized LogsCleanup Trigger: Branch Deleted / MergedAuto-drop DB • Remove Vhost • Clear Cache Keys • Free Disk
Feature branch deployment workflow isolation prevents state collision by giving each branch its own database, cache prefix, and subdomain.

In practice, this means your Deployer 7 configuration must dynamically generate resource names based on the branch slug. On a real client project involving a multi-vendor marketplace, we moved from a single staging environment to per-branch previews and reduced QA-related merge conflicts by nearly 80%. The key is treating the preview environment as disposable infrastructure rather than a persistent pet. Each branch gets its own MySQL database (e.g., app_feat_checkout), its own Redis cache prefix, and ideally its own subdomain via wildcard DNS. This ensures that running migrations or seeding test data for one feature never corrupts another developer's work.

How do you configure Deployer 7 for dynamic preview environments?

Deployer 7 is ideal for this because its PHP-based configuration allows runtime logic that YAML-only tools struggle with. You need to define a dynamic host selector that reads the CI_COMMIT_REF_SLUG variable from GitLab CI. Here is a battle-tested pattern I use for Laravel application deployments:

<?php
// deploy.php
namespace Deployer;

require 'recipe/laravel.php';

// Dynamic host definition based on branch
$branch = getenv('CI_COMMIT_REF_SLUG') ?: 'main';
$isPreview = $branch !== 'main' && $branch !== 'production';

if ($isPreview) {
    host('preview')
        ->setHostname('192.0.2.10')
        ->set('deploy_path', '/var/www/app-preview-' . $branch)
        ->set('branch', $branch)
        ->set('db_name', 'app_preview_' . str_replace('-', '_', $branch))
        ->set('cache_prefix', 'prev_' . substr(md5($branch), 0, 6));
} else {
    host('production')
        ->setHostname('192.0.2.20')
        ->set('deploy_path', '/var/www/app-production')
        ->set('branch', 'main');
}

This configuration solves the routing problem, but you also need to handle environment variables dynamically. Never commit preview .env files. Instead, use Deployer’s dotenv task or inject variables during deployment:

  1. Create a template .env.preview in your repository with placeholders like {{DB_NAME}} and {{CACHE_PREFIX}}.
  2. Add a custom task that processes this template using parse() before running artisan config:cache.
  3. Ensure your Nginx vhost generation task runs conditionally only for preview hosts.
  4. Set appropriate TTLs; preview environments should auto-expire after 7–14 days to prevent disk exhaustion.

A common mistake is forgetting to isolate queue workers. If you run Horizon or Supervisor on the preview server, ensure the queue connection name or prefix includes the branch slug. Otherwise, a job dispatched from feat-emails might be processed by a worker configured for fix-billing, causing subtle data corruption that is extremely difficult to debug.

What GitLab CI pipeline structure supports safe feature deployments?

Your .gitlab-ci.yml must distinguish between validation jobs (lint, test) and deployment jobs. For a feature branch deployment workflow, I recommend a three-stage approach that balances speed with safety. Below is a minimal viable pipeline for Laravel 12 on PHP 8.4:

stages:
  - validate
  - build
  - deploy-preview

variables:
  COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"

lint-and-test:
  stage: validate
  image: php:8.4-cli
  script:
    - composer install --prefer-dist --no-progress
    - ./vendor/bin/pint --test
    - php artisan test --parallel
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH != "main"'

build-assets:
  stage: build
  image: node:22-alpine
  script:
    - npm ci --cache .npm-cache
    - npm run build
  artifacts:
    paths:
      - public/build/
    expire_in: 1 week
  rules:
    - if: '$CI_COMMIT_BRANCH != "main"'

deploy-preview:
  stage: deploy-preview
  image: deployer/deployer:7-php8.4
  dependencies:
    - build-assets
  script:
    - vendor/bin/dep deploy preview --branch=$CI_COMMIT_REF_SLUG -v
  environment:
    name: preview/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.app.example.com
    on_stop: stop-preview
  rules:
    - if: '$CI_COMMIT_BRANCH != "main"'

stop-preview:
  stage: deploy-preview
  script:
    - vendor/bin/dep destroy preview --branch=$CI_COMMIT_REF_SLUG
  when: manual
  environment:
    name: preview/$CI_COMMIT_REF_SLUG
    action: stop
GitLab CI Pipeline: Feature Branch FlowVALIDATEPint + Pest/PHPUnitPHP 8.4 • Parallel TestsBUILDVite Assets CompileNode 22 • Artifact UploadDEPLOY PREVIEWDeployer 7 + SymlinkDynamic Host • Env InjectSTOPManual DestroyCleanup ResourcesRules Gate: Only Non-Main Branches Trigger Preview DeployMain Branch → Separate Production Pipeline (Not Shown)Artifact Strategy✓ Build assets ONCE in CI runner✓ Pass via dependencies to deploy job✗ Never run Node/npm on prod serverEnvironment BindingGitLab tracks URL per branch slugMR widget shows live preview linkAuto-stop on merge/close event
GitLab CI pipeline stages for feature branch deployment workflow ensure assets are built once and deployed atomically to isolated environments.

Note the environment:on_stop directive. This is critical for cost control on cloud VMs or limited VPS resources. Without it, orphaned preview environments accumulate indefinitely. I’ve seen servers crash because 40 abandoned feature branches consumed all available inodes. Always pair creation with automated destruction.

How do you manage database migrations and seeding in ephemeral environments?

Migrations in preview environments require a different mindset than production. You cannot rely on incremental migration history being intact because preview databases are often created fresh or restored from snapshots. For database-driven applications, adopt this strategy:

  • Fresh installs for new branches: Run migrate:fresh --seed instead of migrate. This guarantees schema consistency regardless of what migrations exist locally.
  • Snapshot restoration for large datasets: If seeding takes >2 minutes, maintain a sanitized base dump. Restore it via mysql ... < base_dump.sql then run only pending migrations.
  • Isolated seeders: Tag seeders by domain (UserSeeder, ProductSeeder) so preview environments can load only relevant test data.
  • Never run destructive migrations on shared databases: This is why isolation matters. A dropColumn on a shared staging DB affects everyone.

On a legal-tech portal I built, case documents were too large to seed repeatedly. We solved this by mounting a read-only NFS share containing sanitized document fixtures, while keeping metadata tables ephemeral. This hybrid approach gave developers realistic test data without 20-minute setup times. The tradeoff is operational complexity, but for data-heavy applications, it’s worth it.

When should you choose preview environments over traditional staging?

Not every project needs per-branch deployments. The overhead of wildcard DNS, dynamic Nginx configs, and cleanup scripts only pays off under specific conditions. Use this comparison to decide:

CriteriaTraditional StagingFeature Branch Previews
Team Size1–2 developers3+ concurrent developers
Release CadenceWeekly/monthly monolithic releasesDaily/continuous integration
Data SensitivityCan use shared anonymized copyRequires strict isolation per feature
Infrastructure CostFixed single server (~Rs 3,000/mo)Scales with active branches (~Rs 500/branch/mo)
QA ProcessSequential testing, bottleneck-proneParallel verification, faster feedback
Complexity ToleranceLow ops overhead preferredWilling to invest in DevOps automation
Decision Framework: Do You Need Per-Branch Deploys?START: Evaluate Project≥3 Developers Working Concurrently?NOYESUse Traditional StagingDaily Releases Needed?NOYESStaging + MR Previews HybridFull Preview WorkflowBudget Check: Can You Absorb ~Rs 500–1,500/branch/month?If NO → Revert to Traditional Staging Regardless of Team Size
Decision framework helps determine if a feature branch deployment workflow justifies infrastructure costs for your Nepal-based or global team.

For solo freelancers or small agencies in Nepal working on fixed-budget projects, traditional staging is usually sufficient. The ROI on preview environments kicks in when parallel development becomes the bottleneck, not server costs. However, even small teams benefit from previews when working on high-risk features like payment integrations or legal compliance modules where testing against production-like isolation is non-negotiable.

Implementing Your Feature Branch Deployment Workflow Safely

Start small. Configure one non-critical service first, validate the cleanup automation thoroughly, then expand. Monitor disk usage and database counts aggressively for the first month; leaks in teardown scripts are the most common failure mode. Document the workflow internally so new developers understand that preview URLs are ephemeral and shouldn’t be shared with end clients as permanent links. When implemented correctly, a feature branch deployment workflow transforms your team’s velocity and confidence. If you need help architecting this for your Laravel or Symfony stack, reach out to discuss your deployment challenges.

Frequently Asked Questions

A development practice where every new feature gets its own Git branch and an isolated preview environment for testing before merging to main.

Feature branches isolate work in separate environments per ticket, while trunk-based development merges small changes directly to main frequently with shared staging.

Avoid when release cycles are daily, team size exceeds twenty developers, or infrastructure costs for ephemeral environments outweigh testing benefits.

Define dynamic hosts in deploy.php using the branch name as a subdomain prefix. In my experience maintaining multiple Laravel sites on shared EC2 infrastructure, I map each branch to a unique document root under /var/www/project/branches/{branch-name}. The GitLab CI pipeline triggers dep deploy only on merge request events, passing the branch slug as an environment variable. This avoids polluting production releases while keeping configuration identical across environments through shared .env templates and symlinked storage directories.

Each Laravel preview environment typically requires 512MB RAM and 2GB disk space minimum. On a standard 8GB Ubuntu 24 VPS costing around Rs 3,000 per month (~USD 22), you can safely host ten to twelve concurrent feature branches alongside production. Database isolation matters more than CPU; I create separate MySQL databases prefixed with the branch slug rather than sharing schemas. Monitor disk usage aggressively because abandoned branches accumulate quickly. Set up automated cleanup cron jobs that delete environments older than fourteen days or linked to closed merge requests to prevent resource exhaustion.

Run php artisan migrate:fresh --seed during initial environment creation to ensure schema consistency without legacy data interference. For long-lived feature branches spanning multiple sprints, run incremental migrations instead but always test against fresh seeds first. In production Laravel applications I maintain, migration conflicts cause most preview environment failures. Store seeders specific to feature testing separately from production fixtures. Never share a database between feature branches and staging. If your application uses Spatie Media Library or similar file-dependent packages, also seed the storage directory structure or symlink to shared test assets to prevent broken media references during QA review.

Maintain a base .env.template in version control with non-sensitive defaults, then inject branch-specific secrets via GitLab CI variables or Vault at deploy time. Never commit API keys, database passwords, or payment gateway credentials to feature branches. For Nepal-based projects integrating eSewa or Khalti, use sandbox credentials in preview environments and reserve production keys exclusively for the main branch. In Deployer 7, use the set() function to dynamically write .env files during deployment. Validate environment completeness with php artisan config:cache as part of the pipeline to catch missing variables before testers encounter runtime errors.

Use predictable subdomains like {branch-slug}.preview.yourdomain.com or path-based routing at preview.yourdomain.com/{branch-slug}. Subdomains avoid cookie and session conflicts between branches, which I have found critical when testing authentication flows in legal-tech portals. Configure wildcard DNS (*.preview.yourdomain.com) pointing to your server and add corresponding ServerAlias directives in Apache or Nginx. Ensure SSL covers the wildcard via Let's Encrypt certbot with the --expand flag. Avoid query parameters or port numbers because they complicate OAuth callbacks and webhook testing. Keep slugs lowercase, hyphenated, and under thirty characters to prevent DNS label length violations.

Create a scheduled Artisan command or shell script triggered nightly via cron that queries your Git provider API for closed or merged merge requests, then deletes matching directories, databases, and DNS records. In Deployer 7, define a custom task dep cleanup:branches that removes the document root and runs DROP DATABASE IF EXISTS for each stale slug. On projects I maintain, uncleaned branches consumed over 40GB within three months before automation was added. Retain environments for active merge requests plus a seven-day grace period. Log deletions to track accidental removals and integrate notifications to Slack or email so developers know when their preview environment expires.

Restrict access via HTTP basic auth, IP whitelisting, or SSO integration since preview environments often contain incomplete security controls. Never expose them publicly even if they mirror production code. Disable search engine indexing with X-Robots-Tag: noindex headers and robots.txt disallow rules. Sanitize all user-uploaded content because feature branches may lack production validation. For client-facing demos on legal service platforms, I generate time-limited access tokens rather than sharing static credentials. Rotate secrets after each sprint and audit access logs weekly. Treat every preview environment as potentially compromised and never connect it to production databases, payment gateways, or live third-party APIs.

Compile assets during CI and upload artifacts to the server rather than running Node.js on production infrastructure. In my Deployer 7 workflows, npm run build executes in the GitLab runner, then compiled CSS and JS transfer via rsync to the branch-specific public/build directory. This keeps servers lean and avoids version mismatches between Node 22 LTS locally and whatever runs remotely. Cache node_modules between pipeline runs to reduce build times. For Vue or Livewire projects, ensure manifest.json paths resolve correctly per branch by setting ASSET_URL in .env to the branch subdomain. Test asset loading explicitly because broken manifests are the most common post-deploy failure I encounter in preview environments.

Opcache serving stale PHP files after symlink swaps is the top culprit; always run sudo systemctl reload php8.4-fpm in your deploy hook. Second is hardcoded URLs in config/cache/views that ignore APP_URL overrides. Third is permission mismatches when CI creates files as root but PHP-FPM runs as www-data. Fourth is queue workers processing jobs from the wrong environment due to shared Redis prefixes. Fifth is session driver conflicts when multiple branches use the same database table. Debug systematically by checking opcache status, verifying .env values via tinker, confirming file ownership with ls -la, inspecting queue connection config, and validating session isolation. Most issues trace to environment leakage rather than code defects.

Infrastructure adds Rs 2,000–5,000 monthly (~USD 15–37) for a mid-tier VPS hosting five to eight concurrent environments beyond production. Developer time investment is higher initially: expect two to three days configuring Deployer 7, GitLab CI, DNS, and cleanup automation. Ongoing maintenance averages two hours weekly for troubleshooting and updates. For agencies billing Rs 2,500/hour (~USD 19), that is Rs 20,000 monthly in operational overhead. Weigh this against QA efficiency gains; on eCommerce projects like florist platforms with frequent UI iterations, preview environments reduced staging feedback cycles by sixty percent. For solo developers or infrequent releases, trunk-based development with local Docker may offer better ROI.

Yes but with significant caveats. WordPress lacks native multi-environment support, requiring wp-config.php overrides for DB_NAME, WP_HOME, and WP_SITEURL per branch. Plugins with hardcoded paths or serialized options break frequently when cloned. Use WP-CLI search-replace during environment creation to fix URLs. Media uploads need separate directories or S3 buckets per branch. Theme and plugin symlinks help but complicate updates. In my experience, WooCommerce stores with complex product data and payment integrations are especially fragile in ephemeral environments. Consider using LocalWP or Docker for developer previews and reserve true feature branch deployments for headless WordPress setups where the frontend is decoupled and backend changes are API-driven.

Trigger PHPUnit, Pest, or Playwright suites in GitLab CI immediately after deploying the preview environment, not before. Tests must validate the deployed artifact, not just the source code. Configure test runners to hit the branch subdomain URL and use branch-specific test databases seeded with known fixtures. For Laravel APIs, run HTTP tests against the live preview to catch routing, middleware, and CORS issues invisible in unit tests. Fail the pipeline if critical tests regress and block merge request approval until resolved. On booking system projects, I include end-to-end reservation flow tests that execute against each preview environment. This catches integration failures between frontend forms, backend validation, and database transactions that only manifest in deployed contexts.

Share this article

Quick Contact Options
Choose how you want to connect me: