How to Deploy Laravel with CI/CD Pipeline
Complete CI/CD Guide for Laravel Applications
Setting up continuous integration and deployment (CI/CD) for Laravel applications streamlines your workflow, improves code quality, and enables reliable automated releases. This guide walks through a production-ready pipeline from development to deployment.
If you want a managed environment for the deployment target, compare pricing and features before you wire up the pipeline.
Time Required: 45-60 minutes
Difficulty: Intermediate to Advanced
Prerequisites: Laravel application, Git repository, basic Docker knowledge
Table of Contents
- CI/CD Pipeline Overview
- Prerequisites and Setup
- GitHub Actions Configuration
- GitLab CI Alternative
- Automated Deployment Setup
- Monitoring and Alerts
- Best Practices
CI/CD Pipeline Overview
A complete Laravel CI/CD pipeline typically includes:
Continuous Integration (CI) Stages
- Code checkout: Pull latest code from repository
- Dependency installation: Composer and npm packages
- Code quality: Static analysis with PHPStan, Psalm
- Testing: Unit tests, feature tests, browser tests
- Build assets: Compile CSS/JS with Vite or Mix
- Security scanning: Vulnerability checks
Continuous Deployment (CD) Stages
- Build Docker image: Create production-ready container
- Deploy to staging: Automatic deployment to staging environment
- Integration tests: End-to-end testing on staging
- Deploy to production: Automated or manual promotion
- Health checks: Verify deployment success
Prerequisites and Setup
Required Tools and Accounts
- ✅ Laravel application with Git repository
- ✅ GitHub/GitLab account with repository access
- ✅ CloudPloy account for hosting (Sign up free)
- ✅ Docker installed locally for testing
Laravel Project Structure
Ensure your Laravel project has these essential files:
laravel-app/
├── .github/workflows/ # GitHub Actions workflows
├── docker/ # Docker configuration
├── tests/ # Laravel tests
├── phpunit.xml # PHPUnit configuration
├── composer.json # PHP dependencies
├── package.json # Node.js dependencies
├── vite.config.js # Frontend build configuration
└── .env.example # Environment variables template GitHub Actions Configuration
Step 1: Create Workflow File
Create .github/workflows/deploy.yml in your repository:
name: Laravel CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: laravel_test
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: xdebug
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Copy environment file
run: cp .env.example .env
- name: Install Composer dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Install NPM dependencies
run: npm ci
- name: Generate application key
run: php artisan key:generate
- name: Build assets
run: npm run build
- name: Run PHPStan
run: ./vendor/bin/phpstan analyse
- name: Run tests
run: php artisan test --coverage-clover coverage.xml
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: laravel_test
DB_USERNAME: root
DB_PASSWORD: password
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml Step 2: Build and Deploy Job
Add the deployment job to the same workflow file:
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy to CloudPloy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.CLOUDPLOY_HOST }}
username: ${{ secrets.CLOUDPLOY_USER }}
key: ${{ secrets.CLOUDPLOY_SSH_KEY }}
script: |
cd /var/www/html
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
sudo systemctl reload php8.2-fpm Step 3: Configure Secrets
Add these secrets in GitHub repository settings:
| Secret Name | Description | Value |
|---|---|---|
| CLOUDPLOY_HOST | Server IP address | Your CloudPloy server IP |
| CLOUDPLOY_USER | SSH username | ploy (or your configured user) |
| CLOUDPLOY_SSH_KEY | SSH private key | Your private SSH key |
GitLab CI Alternative
For GitLab users, create .gitlab-ci.yml:
stages:
- test
- build
- deploy
variables:
MYSQL_DATABASE: laravel_test
MYSQL_ROOT_PASSWORD: password
DB_HOST: mysql
test:
stage: test
image: php:8.2-cli
services:
- mysql:8.0
before_script:
- apt-get update -yqq
- apt-get install -yqq git libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev
- docker-php-ext-install mbstring pdo_mysql curl json intl gd xml zip bz2 opcache
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
- apt-get install -y nodejs
script:
- cp .env.example .env
- composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
- php artisan key:generate
- npm ci
- npm run build
- php artisan test
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '
' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $DEPLOY_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- ssh $DEPLOY_USER@$DEPLOY_HOST "cd /var/www/html && git pull origin main && composer install --no-dev --optimize-autoloader && php artisan migrate --force && php artisan config:cache"
only:
- main Automated Deployment Setup
Docker Configuration
Create a Dockerfile for containerized deployments:
FROM php:8.2-fpm-alpine
# Install system dependencies
RUN apk add --no-cache git curl libpng-dev libxml2-dev zip unzip nodejs npm
# Install PHP extensions
RUN docker-php-ext-install pdo pdo_mysql mbstring exif pcntl bcmath gd
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application files
COPY . .
# Install PHP dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Install and build frontend assets
RUN npm ci && npm run build
# Set permissions
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
EXPOSE 9000
CMD ["php-fpm"] Deployment Script
Create scripts/deploy.sh for advanced deployment scenarios:
#!/bin/bash
set -e
echo "Starting deployment..."
# Pull latest changes
git pull origin main
# Install/update dependencies
composer install --no-dev --optimize-autoloader --no-interaction
# Build frontend assets
npm ci --production
npm run build
# Run database migrations
php artisan migrate --force
# Clear and cache configuration
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Restart services
sudo systemctl reload php8.2-fpm
sudo systemctl reload nginx
# Health check
if curl -f http://localhost/health-check; then
echo "Deployment successful!"
else
echo "Deployment failed - health check failed"
exit 1
fi Zero-Downtime Deployment
For production applications, implement zero-downtime deployment:
#!/bin/bash
set -e
APP_NAME="laravel-app"
DEPLOY_PATH="/var/www"
SHARED_PATH="$DEPLOY_PATH/shared"
RELEASES_PATH="$DEPLOY_PATH/releases"
CURRENT_PATH="$DEPLOY_PATH/current"
RELEASE=$(date +%Y%m%d%H%M%S)
RELEASE_PATH="$RELEASES_PATH/$RELEASE"
# Create directories
mkdir -p $RELEASES_PATH $SHARED_PATH/storage $SHARED_PATH/bootstrap/cache
# Clone to new release directory
git clone $GIT_REPO $RELEASE_PATH
cd $RELEASE_PATH
# Install dependencies
composer install --no-dev --optimize-autoloader
npm ci --production && npm run build
# Create symlinks to shared directories
ln -nfs $SHARED_PATH/storage $RELEASE_PATH/storage
ln -nfs $SHARED_PATH/bootstrap/cache $RELEASE_PATH/bootstrap/cache
ln -nfs $SHARED_PATH/.env $RELEASE_PATH/.env
# Run migrations
php artisan migrate --force
# Update current symlink
ln -nfs $RELEASE_PATH $CURRENT_PATH
# Restart services
sudo systemctl reload php8.2-fpm
# Clean up old releases (keep last 5)
cd $RELEASES_PATH && ls -1d */ | head -n -5 | xargs rm -rf
echo "Deployment completed: $RELEASE" Monitoring and Alerts
Health Check Endpoint
Create a health check route in routes/web.php:
Route::get('/health-check', function () {
$checks = [
'database' => false,
'cache' => false,
'storage' => false,
];
try {
// Database check
DB::connection()->getPdo();
$checks['database'] = true;
// Cache check
Cache::put('health-check', 'ok', 10);
$checks['cache'] = Cache::get('health-check') === 'ok';
// Storage check
$checks['storage'] = Storage::disk('local')->put('health-check.txt', 'ok');
Storage::disk('local')->delete('health-check.txt');
} catch (Exception $e) {
Log::error('Health check failed: ' . $e->getMessage());
}
$allHealthy = !in_array(false, $checks);
return response()->json([
'status' => $allHealthy ? 'healthy' : 'unhealthy',
'checks' => $checks,
'timestamp' => now()->toISOString()
], $allHealthy ? 200 : 503);
}); Deployment Notifications
Add Slack notifications to your deployment workflow:
- name: Slack Notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
message: |
${{ job.status == 'success' && '✅' || '❌' }} Laravel deployment ${{ job.status }}
Branch: ${{ github.ref }}
Commit: ${{ github.sha }}
Author: ${{ github.actor }}
if: always() Best Practices
🔒 Security Best Practices
- Environment Variables: Never commit sensitive data to Git
- SSH Keys: Use dedicated deployment keys with limited permissions
- Secrets Management: Rotate deployment secrets regularly
- Access Control: Limit who can trigger deployments
🚀 Performance Optimization
- Caching Strategy: Implement multi-layer caching in CI/CD
- Parallel Jobs: Run tests and builds in parallel
- Artifact Management: Cache dependencies between builds
- Build Optimization: Optimize Docker layers for faster builds
📊 Testing Strategy
| Test Type | When to Run | Purpose |
|---|---|---|
| Unit Tests | Every push | Verify individual components |
| Feature Tests | Every push | Test application features |
| Browser Tests | Before deployment | End-to-end user workflows |
| Load Tests | Scheduled/manual | Performance validation |
🔄 Rollback Strategy
Implement automatic rollback on deployment failure:
- name: Rollback on failure
if: failure()
run: |
echo "Deployment failed, rolling back..."
ssh $DEPLOY_USER@$DEPLOY_HOST "
cd /var/www &&
ln -nfs $(ls -1d releases/*/ | tail -n 2 | head -n 1) current &&
sudo systemctl reload php8.2-fpm
"
echo "Rollback completed" CloudPloy CI/CD Benefits
When you deploy Laravel with CI/CD on CloudPloy, you get:
🚀 Optimized Performance
- Laravel-optimized server configurations
- Automatic OPcache and Redis caching
- SSD storage with high IOPS
- CDN integration for static assets
🛡️ Enhanced Security
- Automatic security updates
- Laravel-specific firewall rules
- SSL certificate automation
- Regular security scans
💼 Developer Experience
- One-click staging environments
- Integrated monitoring dashboards
- Automated backup management
- 24/7 deployment support
Common Issues and Solutions
Issue 1: Deployment Timeouts
Solution: Increase timeout values and optimize build process
timeout-minutes: 30 # Increase from default 10 minutes Issue 2: Database Migration Failures
Solution: Use migration rollback and backup verification
php artisan migrate --force --step
php artisan migrate:status Issue 3: Asset Build Failures
Solution: Use Node.js version management and clean installs
rm -rf node_modules package-lock.json
npm ci Next Steps
After implementing CI/CD for your Laravel application:
- Set up Performance Optimization
- Implement Security Best Practices
- Configure Automated Backups
- Set up Team Notifications
Need Professional Help?
CloudPloy offers managed CI/CD setup services:
- 💬 Expert Consultation: 24/7 DevOps specialists
- 🛠️ Custom Pipeline Setup: Tailored to your needs
- 📚 Team Training: CI/CD best practices workshop
- 🎯 Optimization Services: Performance and security tuning
Deploy Laravel with Professional CI/CD
Experience seamless Laravel deployments with CloudPloy:
- 🚀 Laravel-optimized infrastructure
- 🔄 Automated CI/CD pipeline setup
- 🛡️ Enterprise-grade security
- 💬 24/7 DevOps expert support
- 🎁 Free migration and setup assistance
No credit card required • Expert DevOps setup • Cancel anytime
Last updated: 2025-08-30