How to Set Up Jenkins for PHP Projects

How to Set Up Jenkins for PHP Projects

PHP CI/CD gets less blog coverage than its Java or Node counterparts, which is odd given how much of the web still runs on it. When I set up Jenkins for a legacy Laravel monolith at a previous job, most of the pain wasn’t Jenkins itself – it was getting PHP’s dependency and testing tooling to behave predictably inside CI. Once that was sorted, the pipeline itself was refreshingly simple. Here’s the complete setup, from a bare Jenkins install to a full Composer-Test-Deploy pipeline.

Why Use Jenkins for PHP Projects

PHP projects benefit from CI/CD the same way any codebase does, but a few things matter more here specifically:

  • Composer dependency consistency – locking down exact package versions across every environment.
  • Multiple PHP version support – many teams still maintain code that must run against more than one PHP version (e.g., supporting a WordPress plugin across PHP 8.1-8.3).
  • Static analysis – PHP’s dynamic typing makes tools like PHPStan or Psalm disproportionately valuable in catching bugs before runtime.
  • Framework-specific tooling – Laravel, Symfony, and WordPress each have their own testing and deployment quirks that a pipeline needs to account for.

Step 1: Prerequisites on the Jenkins Agent

Your Jenkins agent needs PHP, Composer, and your testing tools installed. The cleanest way to guarantee consistency is to run builds inside a PHP Docker image rather than installing PHP directly on the agent host.

Step 2: Install Required Jenkins Plugins

From Manage JenkinsPlugins, install:

  • Git Plugin
  • Docker Pipeline Plugin
  • JUnit Plugin (PHPUnit can output JUnit-format XML)
  • HTML Publisher Plugin (for code coverage reports)
  • Warnings Next Generation Plugin (visualizes PHPStan/Psalm/PHP_CodeSniffer output)
java -jar jenkins-cli.jar -s http://localhost:8080/ install-plugin git docker-workflow junit htmlpublisher warnings-ng -restart

Step 3: Write the Jenkinsfile

Here’s a full pipeline for a Laravel-style PHP project, covering dependency installation, static analysis, unit tests, and coverage reporting:

pipeline {
    agent {
        docker {
            image 'php:8.3-cli'
            args '-v composer-cache:/tmp/composer-cache'
        }
    }

    environment {
        COMPOSER_CACHE_DIR = '/tmp/composer-cache'
        APP_ENV = 'testing'
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/myorg/php-app.git'
            }
        }

        stage('Install Dependencies') {
            steps {
                sh 'curl -sS https://getcomposer.org/installer | php'
                sh 'php composer.phar install --no-interaction --prefer-dist --optimize-autoloader'
            }
        }

        stage('Static Analysis') {
            steps {
                sh 'vendor/bin/phpstan analyse app --level=5 --error-format=checkstyle > phpstan-report.xml || true'
            }
        }

        stage('Code Style Check') {
            steps {
                sh 'vendor/bin/phpcs --standard=PSR12 app --report=checkstyle --report-file=phpcs-report.xml || true'
            }
        }

        stage('Run Tests') {
            steps {
                sh 'cp .env.testing .env'
                sh 'php artisan key:generate'
                sh 'php artisan migrate --force'
                sh 'vendor/bin/phpunit --coverage-html coverage --log-junit junit-report.xml'
            }
        }
    }

    post {
        always {
            junit 'junit-report.xml'
            recordIssues tools: [phpStan(pattern: 'phpstan-report.xml'), checkStyle(pattern: 'phpcs-report.xml')]
            publishHTML(target: [
                reportDir: 'coverage',
                reportFiles: 'index.html',
                reportName: 'PHPUnit Coverage Report'
            ])
        }
    }
}

Step 4: Testing Against Multiple PHP Versions

For libraries that need to support several PHP versions, use a matrix build:

pipeline {
    agent none
    stages {
        stage('Test Matrix') {
            matrix {
                axes {
                    axis {
                        name 'PHP_VERSION'
                        values '8.1', '8.2', '8.3'
                    }
                }
                stages {
                    stage('Test') {
                        agent {
                            docker { image "php:${PHP_VERSION}-cli" }
                        }
                        steps {
                            sh 'curl -sS https://getcomposer.org/installer | php'
                            sh 'php composer.phar install --no-interaction'
                            sh 'vendor/bin/phpunit'
                        }
                    }
                }
            }
        }
    }
}

Step 5: Handling a Database in Tests

Most real PHP applications need a database for integration tests. Use Docker Compose or Jenkins’ docker agent with a linked service container:

pipeline {
    agent {
        docker {
            image 'php:8.3-cli'
            args '--network=ci-net'
        }
    }
    stages {
        stage('Start MySQL') {
            steps {
                sh 'docker run -d --name mysql-test --network=ci-net -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=app_test mysql:8.0'
                sh 'sleep 15'
            }
        }
        stage('Run Tests') {
            steps {
                sh 'DB_HOST=mysql-test DB_DATABASE=app_test vendor/bin/phpunit'
            }
        }
    }
    post {
        always {
            sh 'docker rm -f mysql-test || true'
        }
    }
}

Step 6: Deployment

A common PHP deployment pattern is rsync-based deployment to a traditional LAMP server, or building a Docker image for containerized environments.

Traditional server deployment (rsync + SSH):

stage('Deploy') {
    when { branch 'main' }
    steps {
        sshagent(['prod-ssh-key']) {
            sh '''
                rsync -avz --delete \
                  --exclude=.env \
                  --exclude=storage \
                  ./ deploy@prod-server:/var/www/app/
                ssh deploy@prod-server "cd /var/www/app && composer install --no-dev --optimize-autoloader && php artisan migrate --force && php artisan config:cache"
            '''
        }
    }
}

Containerized deployment:

stage('Build and Push Image') {
    when { branch 'main' }
    steps {
        sh 'docker build -t myregistry.com/php-app:${BUILD_NUMBER} .'
        sh 'docker push myregistry.com/php-app:${BUILD_NUMBER}'
    }
}

Integrating with the Wider Toolchain

  • Git/GitHub – trigger builds via webhook on push and PR, and post PHPUnit/PHPStan results back as PR status checks using the GitHub Checks plugin.
  • Docker/Kubernetes – containerize the PHP app with a proper multi-stage Dockerfile (Composer install in a build stage, lean php-fpm runtime in the final stage) for deployment to Kubernetes.
  • Maven-equivalent (Composer) – Composer is to PHP what Maven is to Java; commit composer.lock and always install with --no-dev in production builds to keep the deployed artifact lean.
  • Security tools – run composer audit (built into recent Composer versions) or local-php-security-checker in a dedicated pipeline stage to catch known vulnerable dependencies.

Monitoring and Troubleshooting

  • “class not found” errors in CI but not locally – almost always a stale or missing composer install --optimize-autoloader; regenerate the autoloader explicitly in CI rather than trusting a cached vendor directory.
  • Tests pass locally but fail in Jenkins – check .env.testing values and confirm the database service container is fully ready before tests start (sleep or a proper healthcheck wait loop).
  • Composer install is slow – mount a persistent Composer cache volume (COMPOSER_CACHE_DIR) across builds instead of downloading every package from scratch each run.
  • Memory limit errors during PHPUnit runs – increase memory_limit in a custom php.ini used specifically for CI, since default CLI limits are often too low for large test suites with coverage enabled.

Security Best Practices

  • Run composer audit on every build and fail the pipeline on high-severity findings.
  • Never commit .env files with real secrets; inject them via Jenkins Credentials at deploy time.
  • Use least-privilege SSH deploy keys scoped only to the deployment path, not full server access.
  • Keep PHP versions current – PHP has a defined support lifecycle, and running end-of-life versions in production is a real, common risk.

Best Practices

  • Cache the vendor/ directory or Composer cache directory between builds to cut install time significantly on larger projects.
  • Run static analysis (PHPStan/Psalm) and code style checks (PHP_CodeSniffer) on every PR, not just before release, so issues get caught early and cheaply.
  • Separate “fast” unit tests from “slow” integration/database tests into different pipeline stages so quick feedback isn’t blocked by slower suites.
  • Use --no-dev and --optimize-autoloader for any Composer install destined for production, since dev dependencies and unoptimized autoloading both hurt runtime performance.

FAQs

Does Jenkins support PHPUnit output natively? Yes, PHPUnit can output JUnit-format XML (--log-junit), which the standard JUnit Jenkins plugin reads directly without any special PHP-specific plugin.

Can I run WordPress plugin/theme tests in this same setup? Yes, using wp-cli and the WordPress test suite bootstrap inside your Docker image, following largely the same pattern shown above.

How do I handle Composer private repositories/packages? Store the Composer auth token (e.g., for a private Packagist or GitHub token) as a Jenkins Credential and export it as COMPOSER_AUTH before running composer install.

Is Xdebug needed for coverage reports? Yes, or alternatively PCOV, which is faster and purpose-built for coverage collection; install whichever extension your PHP Docker image doesn’t already include.

Can this pipeline support zero-downtime deployments? Yes, using a symlink-based release strategy (deploy to a timestamped directory, then atomically switch a current symlink), which is a common pattern for traditional PHP hosting.

Summary

Setting up Jenkins for PHP is mostly about getting Composer, PHPUnit, and static analysis tools running consistently inside a containerized PHP environment, then wiring their output into Jenkins’ native reporting via JUnit XML and the Warnings Next Generation plugin. From there, deployment can go either the classic rsync/SSH route for traditional hosting or a full Docker image build for containerized environments. Once this pipeline is in place, PHP’s reputation for “hard to test in CI” mostly disappears – it becomes just another well-behaved pipeline in your Jenkins setup.

References

  • Jenkins Pipeline Syntax reference: https://www.jenkins.io/doc/book/pipeline/syntax/
  • Composer documentation: https://getcomposer.org/doc/
  • PHPUnit documentation: https://docs.phpunit.de/
  • PHPStan documentation: https://phpstan.org/user-guide/getting-started
  • Jenkins Warnings Next Generation Plugin: https://plugins.jenkins.io/warnings-ng/
Total
1
Shares

Leave a Reply

Previous Post
How to Use Jenkins for iOS Continuous Integration

How to Use Jenkins for iOS Continuous Integration

Next Post
How to Run Jenkins in Docker Containers

How to Run Jenkins in Docker Containers

Related Posts