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.

Bitbucket Pipelines CI/CD Guide

By Kokil Thapa | Last reviewed: September 2026

A Laravel app that builds locally but ships through manual FTP uploads will eventually break production. Wiring Laravel, Git, Bitbucket, and npm into one pipeline removes that risk. You push to Bitbucket, the runner installs PHP dependencies with Composer, compiles front-end assets with npm and Vite, runs tests, then deploys through SSH. I use this pattern on legal-tech portals and eCommerce sites where downtime costs real money. This guide covers the YAML, the Bitbucket Pipelines stages concept, caching, manual approval gates, and Deployer-based releases that actually survive Monday traffic.

Before you write YAML, align pipeline stages with how your app is structured. If you run a monolith with queues and scheduled tasks, read modern Laravel architecture best practices first. Pipeline design follows application boundaries, not the other way around.

Laravel + Git + Bitbucket + npmGit PushBitbucketTestPHPUnit/PestBuildnpm + ViteDeployDeployer SSHComposer cachenpm cachePipeline artefactsvendor/ + public/build/Services: MySQL 8.4 + RedisEphemeral DB for CI tests only
End-to-end Laravel Git Bitbucket npm pipeline: Git triggers test and asset build stages, caches speed Composer and npm, artefacts feed deployment

How do you connect Laravel, Git, Bitbucket, and npm in one pipeline?

Laravel 13.x needs PHP 8.3 or higher. Laravel 12.x runs on PHP 8.2+. Most teams I work with target PHP 8.4 or 8.5 on the runner image. Your bitbucket-pipelines.yml must install extensions Laravel expects: mbstring, xml, zip, bcmath, pdo_mysql, and redis. Node.js 26 LTS handles npm 12 and Vite 8.x asset builds. Place the file at the repository root next to composer.json and package.json.

Base configuration for Laravel 12/13

The example below runs on pushes to main and develop. MySQL and Redis run as service containers. MySQL uses a tmpfs mount so migrations stay fast. This matches patterns from the official Bitbucket Pipelines documentation.

# bitbucket-pipelines.yml
image: php:8.4-cli

definitions:
  services:
    mysql:
      image: mysql:8.4
      variables:
        MYSQL_DATABASE: 'testing'
        MYSQL_ROOT_PASSWORD: 'secret'
        MYSQL_USER: 'laravel'
        MYSQL_PASSWORD: 'secret'
    redis:
      image: redis:8-alpine

  caches:
    composer: vendor
    node: node_modules

pipelines:
  branches:
    main:
      - step:
          name: Test and Build
          size: 2x
          caches:
            - composer
            - node
          services:
            - mysql
            - redis
          script:
            - apt-get update && apt-get install -y libzip-dev unzip git curl
            - curl -fsSL https://deb.nodesource.com/setup_26.x | bash -
            - apt-get install -y nodejs
            - docker-php-ext-install zip pdo_mysql bcmath opcache
            - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
            - until mysqladmin ping -h127.0.0.1 -ularavel -psecret --silent; do sleep 1; done
            - composer install --no-interaction --prefer-dist --optimize-autoloader
            - cp .env.ci .env
            - php artisan key:generate
            - php artisan migrate:fresh --force
            - ./vendor/bin/pest --coverage-clover=coverage.xml
            - npm ci && npm run build
          artifacts:
            - vendor/**
            - public/build/**
            - bootstrap/cache/**

A common mistake is skipping .env.ci before Artisan runs. Laravel throws opaque errors when APP_KEY or database credentials are missing. Copy the CI env file explicitly every time. For deeper test setup, see Laravel testing with Pest in CI/CD.

Secrets and environment variables

Never commit production secrets to Git. Store APP_KEY, SSH keys, and API tokens as secured repository or workspace variables in Bitbucket. Mark sensitive values as secured so logs mask them. Merge CI templates at runtime:

script:
  - echo "$APP_KEY_BASE64" | base64 -d >> .env.ci
  - cp .env.ci .env

For broader secret-handling patterns, read how to handle secrets in CI/CD pipelines safely. The same rules apply whether you use Bitbucket, GitLab, or GitHub.

What is the Bitbucket Pipelines stages concept?

Bitbucket Pipelines organises work into steps grouped inside pipelines. A pipeline runs when a trigger fires — usually a Git push or pull request. Each step executes inside a Docker container with its own script block, caches, services, and artefacts. Steps in the same list run sequentially unless you use parallel. Later steps can consume artefacts from earlier ones.

Think of it as a assembly line. Stage one validates PHP. Stage two compiles front-end assets. Stage three deploys only if both pass. This maps cleanly to how Laravel teams split backend and front-end work. Understanding stages prevents you from cramming install, test, build, and deploy into one bloated step that fails opaquely.

Bitbucket Pipelines StagesStep 1Install depsStep 2APHPUnitStep 2Bnpm buildparallel blockStep 3Deploy stagingStep 4Manual prodYAML structurepipelines → branches → step(s) → script / caches / servicesparallel → multiple steps run at once, pipeline waits for allartifacts → files passed to downstream steps
Bitbucket Pipelines stages concept: sequential steps, optional parallel blocks, artefact handoff, and manual production gate

Multi-stage pipeline example

Splitting test and asset compilation into parallel steps cuts wall-clock time. Both restore the same Composer cache independently:

pipelines:
  branches:
    main:
      - step:
          name: Install Dependencies
          caches: [composer, node]
          script:
            - composer install --no-interaction
            - npm ci
          artifacts:
            - vendor/**
            - node_modules/**
      - parallel:
          - step:
              name: Run Tests
              script:
                - ./vendor/bin/pest
          - step:
              name: Build Assets
              script:
                - npm run build
              artifacts:
                - public/build/**
      - step:
          name: Deploy Staging
          deployment: staging
          script:
            - php deployer.phar deploy staging

Artefacts are the glue between stages. Without them, each step starts from a clean checkout. Pass vendor/, public/build/, and compiled config cache forward so deploy steps do not reinstall everything. For Vite-specific tuning, see the Vite config guide for Laravel projects.

How do you set up Bitbucket Pipelines manual step approval?

Production deploys should not fire automatically on every green build. Bitbucket supports manual steps with the trigger: manual keyword. The pipeline pauses until a authorised team member clicks Deploy in the Bitbucket UI. This is your Bitbucket Pipelines manual step approval gate.

pipelines:
  branches:
    main:
      - step:
          name: Test and Build
          script:
            - composer install && npm ci && npm run build
            - ./vendor/bin/pest
          artifacts:
            - vendor/**
            - public/build/**
      - step:
          name: Deploy to Production
          deployment: production
          trigger: manual
          script:
            - php deployer.phar deploy production

Combine deployment: production with trigger: manual for environment-scoped variables and an audit trail. Only users with deployment permissions see the Run button. Staging can stay automatic on every merge to develop. Production waits for human confirmation after tests pass.

On sister legal-tech sites I maintain with Deployer 7 and GitLab CI, the same pattern applies. Bitbucket's native manual trigger is simpler than webhook-based approval systems. Document who may approve production deploys in your runbook. Pair this with a post-deploy health check so approval does not mean blind trust.

How do you optimise Composer and npm cache in Bitbucket Pipelines?

Uncached Laravel pipelines often exceed ten minutes. Most of that is redundant composer install and npm ci. Bitbucket provides named caches, but default branch-based keys cause misses across feature branches. Tie cache invalidation to lock files instead.

No cache: 8m 45sComposer 3mnpm 2m 45sTests 1m 30sVite 1m 10sCache hit: 3m 02sComposernpmTests 1m 30sVite 1m 10s65% fasterCache key tied to lock filesInvalidate when composer.lock or package-lock.json changesSame deps across branches share one cache
Composer and npm caching in Bitbucket Pipelines cuts Laravel build times when lock files drive cache keys

Cache definitions that actually hit

definitions:
  caches:
    composer: vendor
    node: node_modules

Run composer validate --strict before install to catch lock file drift early. For more tactics, read CI/CD caching for fast Composer and npm installs. Use the JSON formatter tool to inspect pipeline log payloads when cache restore fails silently.

What is the best zero-downtime deploy strategy for Laravel on Bitbucket?

PHP apps deploy best with symlink swaps, not in-place overwrites. Deployer 7 creates timestamped release directories, links shared .env and storage/, runs migrations, then atomically points current at the new release. I've used this on production Laravel applications for years without user-visible downtime during deploys.

/var/www/appcurrent → release20260912releases/todayreleases/yesterdayshared/.envDeploy sequence1. Upload artefacts to new release dir2. Symlink shared storage and .env3. migrate --force, config:cache4. ln -sfn (atomic swap)5. Reload PHP-FPM, prune old releases
Zero-downtime Laravel deployment via Deployer: shared config persists, symlink swap switches traffic instantly

Deployer step in Bitbucket

- step:
    name: Deploy Production
    deployment: production
    trigger: manual
    script:
      - apt-get update && apt-get install -y openssh-client rsync
      - mkdir -p ~/.ssh && chmod 700 ~/.ssh
      - echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
      - chmod 600 ~/.ssh/id_ed25519
      - ssh-keyscan -H your-server.com >> ~/.ssh/known_hosts
      - curl -LO https://deployer.org/deployer.phar
      - php deployer.phar deploy production --no-interaction

See the full Deployer 7 getting started guide for recipe configuration. For Laravel-specific recipes, read zero-downtime deployment for Laravel with Deployer and the Laravel production deployment checklist.

Post-deploy health check

- step:
    name: Verify Deployment
    script:
      - curl -sf https://your-app.com/api/health | grep -q '"status":"ok"'

This catches broken migrations, missing env vars, and opcache staleness. A green deploy step means nothing if HTTP returns 500. On a legal-tech portal I built, this gate prevented a bad config cache from reaching clients.

How do Bitbucket Pipelines compare to GitHub Actions and GitLab CI?

Platform choice depends on where your Git repos live, free-minute budgets, and team tooling. All three run Laravel, Git, and npm workflows well. Differences show up in pricing, marketplace actions, and self-hosted runner flexibility.

CriteriaBitbucket PipelinesGitHub ActionsGitLab CI/CD
Free tier minutes50 min/month per workspace2,000 min/month on public repos400 min/month on shared runners
Manual deploy gatesNative trigger: manualEnvironment protection rulesManual jobs in rules
PHP ecosystemDocker images, custom scriptssetup-php action, large marketplaceStrong PHP templates
Jira integrationNative Atlassian stackThird-party appsThird-party apps
Self-hosted runnersLinux, macOS, WindowsAll major platformsMost flexible options
Best fitAtlassian/Jira teamsOpen source, GitHub-centric shopsSelf-managed DevOps teams

Teams on Jira often pick Bitbucket because branch and deployment status sync into tickets automatically. If you already run GitLab elsewhere, compare against the GitLab CI/CD for Laravel step-by-step guide and GitHub Actions vs GitLab CI comparison. Exceeding free tiers costs Rs 1,500–5,000/month (~USD 11–37) at current rates. Track usage in the dashboard. See the Nepal income tax guide for freelancers for deducting infrastructure spend.

How do you troubleshoot common Bitbucket Pipelines failures?

Most failures fall into four buckets: memory limits, flaky services, permission errors, and cache corruption. Knowing which bucket saves hours.

Memory and step size

Laravel test suites and Vite builds can exceed the default 1 GB allocation. OOM kills show as exit code 137 or plain "Killed". Use size: 2x for 4 GB on Laravel projects:

- step:
    size: 2x
    script:
      - ./vendor/bin/pest

Available sizes in 2026: 1x (1 GB), 2x (4 GB), 4x (8 GB), 8x (16 GB). Larger sizes burn minutes faster. Profile before jumping to 8x.

Service container timing

MySQL needs a few seconds after the container starts. Without a wait loop, tests fail intermittently with connection refused:

script:
  - until mysqladmin ping -h127.0.0.1 -ularavel -psecret --silent; do sleep 1; done
  - php artisan migrate:fresh --force

Deterministic waits beat retry decorators for database readiness. Flaky pipelines erode team trust faster than slow pipelines.

File permissions on deploy

Pipeline containers run as root. Production PHP-FPM often runs as www-data. Fix ownership after upload:

script:
  - chown -R www-data:www-data storage/ bootstrap/cache/
  - chmod -R 775 storage/ bootstrap/cache/

Server-side issues like this overlap with Linux system administration for production PHP hosts. Wrong permissions are a top reason Laravel works in CI but fails after deploy.

When to call for help

Multi-app setups, custom runners, or compliance requirements add complexity fast. A CI/CD pipeline setup expert in Nepal can short-circuit weeks of trial and error. For application delivery beyond pipelines, see web development services and examples like Court Marriage In Nepal — a Laravel legal portal shipped with automated deploys.

Key Takeaways

  • Place bitbucket-pipelines.yml at the repo root and wire Laravel (Composer), Git triggers, Bitbucket runners, and npm (Vite) in separate logical steps.
  • Use the Bitbucket Pipelines stages concept: sequential steps for install, parallel blocks for test vs build, artefacts to pass vendor/ and public/build/ forward.
  • Add trigger: manual on production deploy steps for Bitbucket Pipelines manual step approval before traffic hits new code.
  • Cache vendor and node_modules with keys tied to lock files — expect roughly 60% faster builds on cache hits.
  • Deploy with Deployer symlink swaps, reload PHP-FPM after the atomic current switch, and curl a health endpoint before marking success.
  • Store secrets as secured Bitbucket variables; never commit .env production values to Git.

People Also Ask

Does Bitbucket Pipelines support Laravel and npm in the same pipeline?

Yes. One pipeline can run PHP steps for Composer and Artisan, then Node steps for npm ci and npm run build. Install Node inside the PHP image or split work across parallel steps that share cached dependencies through artefacts.

What are Bitbucket Pipelines stages?

Stages are steps grouped under a pipeline trigger. Each step runs in its own container with a script, optional caches, services, and artefacts. Sequential steps wait for the prior step. Parallel blocks run simultaneously. Manual steps pause until a human approves.

How do I require manual approval before production deploy?

Add trigger: manual to the production deploy step and set deployment: production. Bitbucket shows a Run button only to users with deployment permissions. Pair this with branch restrictions so only main can reach that step.

How many free build minutes does Bitbucket Pipelines include?

Free workspaces get 50 build minutes per month on the standard tier as of 2026. Paid plans add more minutes and larger step sizes. Monitor usage under Repository settings → Pipelines → Usage. Self-hosted runners consume no cloud minutes.

Ship Laravel with confidence through Bitbucket Pipelines

Connecting Laravel, Git, Bitbucket, and npm in one pipeline is straightforward once you respect stages, caching, and manual production gates. Start with test and build on every push. Add Deployer deploys with symlink swaps. Verify health after each release. Treat your YAML like application code — review changes in pull requests and document non-obvious decisions.

Need help wiring pipelines for an existing Laravel project or migrating from manual FTP deploys? Contact us to discuss CI/CD setup, or reach out directly with your repository details and current hosting stack.

Frequently Asked Questions

Bitbucket Pipelines is a cloud-based CI/CD service integrated directly into Bitbucket repositories. It uses Docker containers to run build, test, and deployment steps defined in a bitbucket-pipelines.yml file at your repo root. Every push or pull request triggers the configured pipeline automatically without external Jenkins servers.

Free tier includes 50 build minutes monthly. Paid plans start around USD 3 per user/month for additional minutes. For Nepal agencies billing clients in NPR, expect Rs 400–600 per developer monthly. Overage charges apply if you exceed included minutes, so monitor usage closely during initial setup and testing phases.

GitHub Actions offers more community actions and faster Windows/macOS runners. Bitbucket Pipelines integrates tighter with Jira and Bitbucket Cloud repos, reducing context switching for teams already in that ecosystem. For pure Laravel deployments to Linux servers via Deployer, both work equally well; choose based on existing repository location and team workflow preferences rather than technical superiority.

Generate an ED25519 key pair locally using ssh-keygen -t ed25519. Add the private key as a secured repository variable named DEPLOY_SSH_KEY in Bitbucket settings. Add the public key to your target server's authorized_keys file. Reference the variable in your pipeline script using echo $DEPLOY_SSH_KEY | base64 -d > /tmp/deploy_key && chmod 600 /tmp/deploy_key before running deployment commands.

Yes. Install Deployer via Composer in your pipeline step, then run dep deploy production. Store SSH keys and environment variables as secured repository variables. Ensure your pipeline image has PHP 8.2+ and required extensions matching your production server. I have used this exact pattern on multiple legal-tech portals sharing infrastructure, achieving zero-downtime symlinked releases consistently across sister sites.

Common causes include incorrect key format, missing newline at end of private key variable, wrong file permissions on the temporary key file, or SSH host key verification failures. Always set chmod 600 on the key file and add ssh -o StrictHostKeyChecking=no to bypass interactive prompts. Verify the key works locally first before debugging inside the container environment where error messages are less verbose.

Use the caches keyword in your bitbucket-pipelines.yml definition. Specify composer under caches for your PHP step to persist the vendor directory between builds. This reduces install time from minutes to seconds on subsequent runs. Note that cache invalidation happens automatically when composer.lock changes, ensuring dependency updates trigger fresh installs while unchanged locks reuse cached artifacts efficiently.

Use php:8.2-cli or php:8.3-cli as your base image since Laravel 12 requires PHP 8.2 minimum. Install required extensions like pdo_mysql, mbstring, xml, zip, and bcmath via apt-get in a custom Dockerfile or inline script. Avoid latest tags; pin specific versions for reproducible builds. Node.js 22 LTS should be added separately if frontend asset compilation is needed during the pipeline execution.

Store sensitive values like database passwords, API keys, and SSH credentials as secured repository or workspace variables in Bitbucket settings, never committed to code. Mark them as secured to mask values in logs. Access them as standard environment variables in pipeline scripts. Rotate credentials regularly and audit variable access through Bitbucket audit logs to maintain security compliance for client projects handling legal or financial data.

Run php artisan migrate --force only after successful deployment and health checks, not during build steps. Use maintenance mode to prevent user access during schema changes. Always backup databases before migration in production pipelines. For Laravel apps, consider running migrations in a separate post-deploy step with rollback capability. Never run destructive migrations without tested down methods and verified restore procedures in place.

Enable verbose output by adding -v flags to commands. Use echo statements before critical operations to verify variable values and paths. Download artifacts from failed builds to inspect generated files. Reproduce failures locally using the same Docker image specified in your pipeline. Check Bitbucket Pipeline logs for truncated error messages; sometimes failures occur silently in shell scripts without proper exit code propagation causing misleading success indicators downstream.

Yes, define multiple parallel steps under the parallel keyword in your YAML configuration. Split test suites by directory or group across concurrent containers to reduce total runtime. Each parallel step gets its own isolated environment, so shared state requires external services like Redis or database fixtures. Monitor minute consumption carefully since parallel steps consume build minutes simultaneously, potentially exhausting free tier limits faster than sequential execution would.

Add the atlassian/slack-notify pipe after your deployment or test steps. Configure SLACK_WEBHOOK_URL as a secured repository variable. Customize message templates to include branch name, commit hash, and deployment environment. Trigger notifications only on failure for production deployments to avoid noise, but notify on all statuses for staging environments during active development sprints. Test webhook connectivity in a non-critical pipeline first to validate formatting before relying on alerts for production monitoring workflows.

Environment differences between local machines and Docker containers cause most issues. Missing system packages, different PHP versions, absent SSH agents, and hardcoded absolute paths break automated flows. File permissions inside containers differ from host systems. Cron jobs referencing release paths need updating after symlink swaps. Start by automating tests before attempting deployments. Validate every assumption about your environment explicitly in pipeline scripts rather than assuming parity with your development machine setup.

Use custom pipelines with variables to differentiate staging, production, and testing targets. Define separate deployment steps triggered manually or by branch patterns. Store environment-specific configurations as distinct secured variables prefixed with environment names. Use conditional logic or separate pipeline definitions for each target. On projects with shared infrastructure like legal service portals, I maintain single pipeline files with parameterized deploy targets to reduce duplication while keeping environment isolation strict and auditable.

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: