Bitbucket Pipelines for Laravel: CI/CD Setup Guide
Set up Bitbucket Pipelines for Laravel: run tests on every pull request and deploy to your server automatically on merge, with a full YAML file explained.
Bitbucket Pipelines for Laravel gives you automated tests on every pull request and a hands-off deploy every time you merge to main. Manual deployments with FTP or "SSH in and git pull" eventually break something on a Friday afternoon. A pipeline makes every deploy identical and every pull request tested.
I have moved several client projects from manual uploads to Bitbucket Pipelines. In this guide I share the complete bitbucket-pipelines.yml I start from, explain each part, add a MySQL service for real database tests, build front-end assets, and deploy over SSH with a safe script on the server. You will also see the errors I hit most often and how to fix them.
Prerequisites
- A Laravel 11 project (PHP 8.3) in a Bitbucket Cloud repository
- A test suite that runs with php artisan test (PHPUnit or Pest)
- A Linux server with SSH access, Git, Composer, PHP 8.3 and Node 20
- Pipelines enabled under Repository settings, then Pipelines, then Settings
How the Laravel CI/CD pipeline works
- A developer opens a pull request, Pipelines runs the test step
- Bitbucket shows a green or red check on the pull request, you can require it before merging
- When the pull request merges into main, tests run again on the merged code
- If tests pass, a deploy step connects to the server over SSH and runs a deploy script
- Bitbucket records the deployment in the Deployments dashboard so you can see what is live
Step 1: Create bitbucket-pipelines.yml for Laravel
Put this file in the root of the repository. It uses a YAML anchor so the test step is defined once and reused for pull requests and for main. The MySQL service gives tests a real database instead of SQLite, which catches MySQL-specific query issues.
image: php:8.3-cli
definitions:
services:
mysql:
image: mysql:8.0
variables:
MYSQL_DATABASE: testing
MYSQL_ROOT_PASSWORD: secret
caches:
npm: ~/.npm
steps:
- step: &test
name: Test
caches:
- composer
- npm
services:
- mysql
script:
- apt-get update && apt-get install -y git unzip libzip-dev libpng-dev curl
- docker-php-ext-install pdo_mysql zip gd bcmath
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs
- composer install --no-interaction --prefer-dist --no-progress
- cp .env.example .env
- php artisan key:generate
- npm ci && npm run build
- php artisan test
artifacts:
- storage/logs/**
pipelines:
pull-requests:
'**':
- step: *test
branches:
main:
- step: *test
- step:
name: Deploy to production
deployment: production
trigger: automatic
script:
- pipe: atlassian/ssh-run:0.8.1
variables:
SSH_USER: 'deploy'
SERVER: $PROD_HOST
MODE: 'command'
COMMAND: 'cd /var/www/app && ./deploy.sh'Step 2: Point the test environment at the MySQL service
Services run on 127.0.0.1 inside the pipeline. Set the database variables in phpunit.xml so tests do not depend on .env values. If you prefer SQLite for speed, use DB_CONNECTION sqlite and DB_DATABASE :memory: and drop the service.
<!-- phpunit.xml -->
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="mysql"/>
<env name="DB_HOST" value="127.0.0.1"/>
<env name="DB_PORT" value="3306"/>
<env name="DB_DATABASE" value="testing"/>
<env name="DB_USERNAME" value="root"/>
<env name="DB_PASSWORD" value="secret"/>
<env name="CACHE_STORE" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="MAIL_MAILER" value="array"/>
</php>Step 3: Add SSH keys and repository variables
In Repository settings, open Pipelines, then SSH keys, and generate a key pair. Copy the public key into ~/.ssh/authorized_keys for the deploy user on your server. Then fetch and add the server's host fingerprint on the same page so the pipeline trusts the host.
Under Repository variables (or Deployment variables for production only) add PROD_HOST with your server IP or hostname. Mark sensitive values as Secured so they are masked in logs. Never commit .env or keys to the repository.
Step 4: The deploy script on the server
This script lives in the repository root and runs on the server. set -e stops on the first error, and the trap brings the site back up even if a step fails, so you never leave production stuck in maintenance mode.
#!/bin/bash
set -e
cd /var/www/app
php artisan down --retry=60 || true
trap 'php artisan up' EXIT
git fetch origin main
git reset --hard origin/main
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
npm ci
npm run build
php artisan migrate --force
php artisan optimize:clear
php artisan optimize
php artisan queue:restart
echo "Deployed $(git rev-parse --short HEAD) at $(date)"Step 5: Make the deploy script executable and prepare the server
Git stores the executable bit, so set it once and commit. On the server, the deploy user must own the app directory and be able to reload PHP-FPM if you need that step.
git update-index --chmod=+x deploy.sh
git commit -m "Make deploy script executable"
# on the server, once
sudo adduser deploy
sudo chown -R deploy:www-data /var/www/app
sudo chmod -R 775 /var/www/app/storage /var/www/app/bootstrap/cacheStep 6: Zero-downtime Laravel deployment (optional)
The script above has a few seconds of maintenance mode. For busy apps I switch to release folders: build each release in its own directory, then swap a symlink atomically. Tools like Deployer or Envoy do this for you, but the idea is simple.
RELEASE=/var/www/releases/$(date +%Y%m%d%H%M%S)
git clone --depth 1 git@bitbucket.org:team/app.git "$RELEASE"
cd "$RELEASE"
ln -s /var/www/shared/.env .env
rm -rf storage && ln -s /var/www/shared/storage storage
composer install --no-dev --optimize-autoloader --no-interaction
npm ci && npm run build
php artisan migrate --force
php artisan optimize
ln -sfn "$RELEASE" /var/www/current
php artisan queue:restart
sudo systemctl reload php8.3-fpmCommon Bitbucket Pipelines errors and fixes
- Host key verification failed: the server fingerprint was not added under SSH keys, fetch it in the Pipelines SSH settings
- SQLSTATE[HY000] [2002] Connection refused: DB_HOST is localhost or mysql, use 127.0.0.1 for services
- Exceeded memory limit or build minutes: cache composer and npm, and avoid installing unused PHP extensions
- Your requirements could not be resolved: the image PHP version does not match composer.json, pin php:8.3-cli
- Vite manifest not found during tests: run npm run build before php artisan test, or call $this->withoutVite() in tests
- Permission denied on deploy.sh: the executable bit is missing, use git update-index --chmod=+x
Best practices for Laravel CI/CD
- Require a passing pipeline before merging in branch restrictions
- Use Deployment variables so production secrets are only available to the deploy step
- Keep the deploy script in the repository so changes are reviewed like code
- Build a custom Docker image with extensions preinstalled to cut minutes from every build
- Add a staging branch and deployment that mirrors production
- Run php artisan migrate --force only with backward-compatible migrations during zero-downtime deploys
Conclusion
Bitbucket Pipelines for Laravel takes an afternoon to set up and saves you from every "it worked on my machine" deploy afterwards. Start with the test step on pull requests, add the SSH deploy for main, and move to release folders when downtime starts to matter. On the projects where I introduced it, failed deploys went from a monthly event to practically never.
Frequently asked questions
Is Bitbucket Pipelines free for Laravel projects?
Bitbucket Cloud's free plan includes a monthly allowance of build minutes, which is enough for small projects. Caching dependencies keeps usage low.
How do I run Laravel tests with MySQL in Bitbucket Pipelines?
Define a mysql service under definitions, attach it to the test step, and connect with DB_HOST 127.0.0.1. Install the pdo_mysql extension in the build image.
How do I deploy Laravel from Bitbucket Pipelines to a VPS?
Add an SSH key in the Pipelines settings, put its public key on the server, and use the atlassian/ssh-run pipe to execute a deploy script. The script pulls code, installs dependencies, migrates and caches config.
Can I deploy only when tests pass?
Yes. Steps run in order and the pipeline stops on the first failure, so placing the deploy step after the test step guarantees it only runs on green builds.
Should I run npm build on the server or in the pipeline?
Building in the pipeline is cleaner because the server needs no Node. You then ship the built assets via an artifact and rsync instead of git pull.