Backend and API Development·9 min

Building a Modern CI/CD Pipeline for Laravel with GitHub Actions and Docker

By Bahaj Abderrazak·Published April 22, 2024·Updated September 24, 2026

Manual FTP uploads and SSH deployment commands are recipes for production outages. Here is how to build an automated, zero-downtime CI/CD pipeline for Laravel using GitHub Actions and Docker.

Deploying software manually — whether by dragging files over FTP, running git pull directly on production servers, or manually executing database migrations over SSH — is one of the most common causes of downtime and human error in engineering teams.

A robust Continuous Integration and Continuous Deployment (CI/CD) pipeline ensures that every commit is tested, linted, analyzed for type safety, and deployed automatically without interrupting active users.

In this guide, I share the GitHub Actions pipeline I use to deliver zero-downtime deployments for production Laravel applications.

Pipeline Architecture Overview

A professional deployment pipeline consists of two stages:

  1. Continuous Integration (CI): Runs on every Pull Request.
    • Code formatting check (Laravel Pint)
    • Static analysis (PHPStan / Larastan at Level 6+)
    • Automated test suite execution (Pest / PHPUnit)
  2. Continuous Deployment (CD): Runs on merge to main.
    • Build optimized production Docker image
    • Push image to GitHub Container Registry (GHCR)
    • Trigger zero-downtime rolling update on VPS / Kubernetes
Developer Push
      │
      ▼
┌──────────────────┐
│ GitHub Actions CI│ ── Lint (Pint) & Static Analysis (PHPStan)
└────────┬─────────┘ ── Automated Test Suite (Pest/PHPUnit)
         │ All Pass
         ▼
┌──────────────────┐
│ Docker Build     │ ── Multi-stage build with OPcache enabled
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Production VPS   │ ── Zero-downtime deployment & DB migrations
└──────────────────┘

Part 1: GitHub Actions CI Workflow

Create .github/workflows/ci.yml:

name: Continuous Integration

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  laravel-tests:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: testing
          POSTGRES_USER: user
          POSTGRES_PASSWORD: password
        ports:
          - 5432:5432
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, pdo, pdo_pgsql, bcmath, redis, zip
          coverage: none

      - name: Cache Composer dependencies
        uses: actions/cache@v3
        with:
          path: vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Install Dependencies
        run: composer install --prefer-dist --no-interaction --no-progress

      - name: Check Code Style (Pint)
        run: ./vendor/bin/pint --test

      - name: Static Analysis (PHPStan)
        run: ./vendor/bin/phpstan analyse --memory-limit=2G

      - name: Run Test Suite
        env:
          DB_CONNECTION: pgsql
          DB_HOST: 127.0.0.1
          DB_PORT: 5432
          DB_DATABASE: testing
          DB_USERNAME: user
          DB_PASSWORD: password
        run: php artisan test --parallel

Part 2: Production Multi-Stage Dockerfile

A production PHP Docker image must be lean, secure, and pre-warmed with OPcache. Use a multi-stage Dockerfile:

# Stage 1: Build assets and vendor
FROM php:8.3-fpm-alpine AS base

RUN apk add --no-cache postgresql-dev libzip-dev zip unzip linux-headers \
    && docker-php-ext-install pdo pdo_pgsql opcache

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

# Install production dependencies only
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts

# Copy application files
COPY . .

# Run production optimizations
RUN php artisan config:cache \
    && php artisan route:cache \
    && php artisan view:cache

EXPOSE 9000
CMD ["php-fpm"]

Part 3: Automated Zero-Downtime Deployment

Once tests pass and the Docker image is pushed to GHCR, GitHub Actions securely triggers the deployment script on the production server via SSH:

# Zero-downtime deployment script on server
docker compose pull app
docker compose up -d --no-deps --build app
docker compose exec -T app php artisan migrate --force
docker compose exec -T app php artisan queue:restart

Because Docker Compose replaces containers only when the new container passes health checks, users never experience dropped requests or maintenance screens during release.

For testing best practices, read my Laravel Unit and Feature Testing Guide and Laravel vs Django vs .NET Comparison.

Need an expert DevOps and backend engineer to automate your deployment pipelines or modernize your infrastructure? Learn more about my backend development services or reach out to discuss your CI/CD setup.

LaravelCI/CDGitHub ActionsDockerDevOpsBackend Development

Related articles

Let's begin

Have an idea worth building?

Tell me what you are creating, what stage the project is at, and where you need technical support — I will respond with practical next steps.