
August 16, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
You need to build a CI/CD pipeline with Jenkins because manual deployments are the single biggest source of production outages and developer burnout in small-to-mid-sized teams. While I now default to GitLab CI for most new projects due to its integrated repository management, Jenkins remains the industry standard for complex, multi-environment workflows where you need granular control over build agents and legacy system integration. This guide skips the marketing fluff and walks you through configuring a secure, production-grade Jenkins instance specifically tuned for PHP and Laravel applications in 2026.
How do you install and secure Jenkins on Ubuntu 24.04?
A common mistake I see when developers first attempt to set up professional CI/CD pipelines is installing Jenkins directly from the default Ubuntu repositories or running it as root. Both approaches lead to version mismatches and severe security vulnerabilities. In 2026, you should always use the official Jenkins APT repository to get the latest Long Term Support (LTS) release and run the service under a dedicated user with restricted permissions.
Before installing Java or Jenkins, ensure your server has at least 4GB of RAM and 2 vCPUs. Jenkins itself is lightweight, but the build processes for modern PHP applications with Composer and Node.js assets will quickly exhaust smaller instances. Start by adding the official repository key and source list to guarantee you receive verified packages:
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc]" \
https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
/etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt update
sudo apt install openjdk-17-jdk jenkins Once installed, never expose port 8080 directly to the internet. Configure Nginx as a reverse proxy with Let’s Encrypt SSL. This allows you to enforce HTTPS, add IP whitelisting via UFW, and handle rate limiting at the web server level before requests ever reach the Java application. Your Nginx configuration should include proxy headers to preserve the original host and protocol, preventing redirect loops and CSRF errors during login:
server {
listen 443 ssl http2;
server_name jenkins.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/jenkins.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/jenkins.yourdomain.com/privkey.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Required for WebSocket support in Jenkins UI
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_set_header Connection "";
}
} Initial security hardening checklist
- Disable signup: Navigate to Manage Jenkins → Security → Authorization and select "Logged-in users can do anything" only if you have SSO configured; otherwise use "Matrix-based security".
- Enable CSRF protection: Ensure "Prevent Cross Site Request Forgery exploits" is checked in Security settings. Modern Jenkins enables this by default, but verify after upgrades.
- Restrict script console: Only grant Script Console access to senior DevOps engineers. This Groovy shell has full system access and is a frequent attack vector.
- Configure audit logging: Install the Audit Trail plugin to track configuration changes and authentication events for compliance and debugging.
Why should you use Docker agents instead of building on the controller?
Building directly on the Jenkins controller is an anti-pattern that causes environment drift, dependency conflicts, and security risks. When you build a CI/CD pipeline with Jenkins for PHP projects, you inevitably need multiple PHP versions (8.2, 8.3, 8.4), different Node.js versions for frontend assets, and various system libraries for PDF generation or image processing. Installing all these on a single controller creates an unmaintainable mess that breaks when any package updates.
Docker agents solve this by providing ephemeral, reproducible build environments. Each pipeline run spins up a fresh container with exactly the dependencies specified in your Dockerfile, runs the build, and destroys itself afterward. This guarantees that builds are identical whether they run on Monday morning or Friday night, and prevents one project's dependencies from contaminating another's.
Install Docker on your Jenkins server and configure the Docker Cloud plugin. Create agent templates for your common build profiles rather than maintaining monolithic images. For Laravel applications, I maintain separate lightweight images for testing (PHP + SQLite/MySQL client) and asset compilation (Node.js + Vite):
# Dockerfile.php84-laravel
FROM php:8.4-cli-alpine
RUN apk add --no-cache git unzip libzip-dev icu-dev oniguruma-dev \
&& docker-php-ext-install pdo_mysql zip intl mbstring opcache \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Pre-warm composer cache layer
COPY composer.json composer.lock /tmp/
RUN cd /tmp && composer install --no-scripts --no-autoloader --prefer-dist \
&& rm -rf /tmp/*
WORKDIR /app When configuring Docker clouds in Jenkins, set the instance cap appropriately. On a 4-core build server, running more than 2-3 concurrent containers typically causes memory pressure and slower builds overall. Use volume mounts for Composer and npm caches to avoid re-downloading dependencies on every build, but mount them as read-write only in specific cache directories, not the entire workspace.
How do you write an optimized Jenkinsfile for Laravel applications?
The Jenkinsfile is where theory meets reality. Many tutorials show simplistic examples that fail in production because they ignore caching, parallel execution, and proper artifact handling. When you build a CI/CD pipeline with Jenkins for Laravel, your pipeline must handle Composer dependencies, database migrations for testing, frontend asset compilation, and deployment artifacts efficiently.
Use declarative pipeline syntax for readability and built-in validation. Structure your stages to maximize parallelism where safe and sequential execution where order matters. Here is a battle-tested Jenkinsfile pattern I've refined across multiple legal-tech and e-commerce projects:
pipeline {
agent none
environment {
COMPOSER_CACHE_DIR = '/tmp/composer-cache'
NPM_CONFIG_CACHE = '/tmp/npm-cache'
APP_ENV = 'testing'
DB_CONNECTION = 'sqlite'
DB_DATABASE = ':memory:'
}
options {
timeout(time: 20, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('Dependencies') {
agent { label 'php84' }
steps {
sh 'composer install --no-interaction --prefer-dist --optimize-autoloader'
stash includes: 'vendor/', name: 'vendor'
}
}
stage('Tests & Assets') {
parallel {
stage('PHPUnit') {
agent { label 'php84' }
steps {
unstash 'vendor'
sh 'php artisan test --parallel --coverage-clover=coverage.xml'
}
post {
always { junit 'tests/JUnit/*.xml' }
}
}
stage('Frontend Build') {
agent { label 'node22' }
steps {
sh 'npm ci --cache ${NPM_CONFIG_CACHE}'
sh 'npm run build'
stash includes: 'public/build/', name: 'assets'
}
}
}
}
stage('Deploy Staging') {
when { branch 'develop' }
agent { label 'deployer' }
steps {
unstash 'vendor'
unstash 'assets'
sh './vendor/bin/dep deploy staging --ansi -vvv'
}
}
}
post {
failure {
// Slack/email notification logic here
}
cleanup {
cleanWs(cleanWhenNotBuilt: true)
}
}
} Critical optimization techniques
- Stash/unstash over archiveArtifacts: Stashing transfers files between nodes within the same build without persisting to disk. Use it for vendor directories and compiled assets that downstream stages need but don't require permanent storage.
- Parallel testing and asset building: Frontend compilation and PHPUnit tests have no dependencies on each other. Running them simultaneously cuts pipeline time by 30-50% on typical Laravel projects.
- In-memory SQLite for tests: Unless your tests specifically require MySQL features like stored procedures or spatial indexes, use SQLite in-memory databases. They eliminate network latency and setup overhead, reducing test suite runtime significantly.
- Explicit timeouts and log rotation: Rogue builds can hang indefinitely and consume disk space. Always set aggressive timeouts and limit retained build history to prevent controller disk exhaustion.
What deployment strategy works best for zero-downtime releases?
Jenkins should orchestrate deployment, not execute it directly via SSH commands scattered throughout your pipeline. This separation of concerns makes deployments testable, rollback-capable, and consistent across environments. For PHP applications, Deployer 7 remains the gold standard in 2026 because it handles atomic symlink swaps, shared file persistence, and graceful PHP-FPM reloading out of the box.
Your Jenkins pipeline should treat deployment as a distinct stage that invokes Deployer (or Ansible/Capistrano) with environment-specific parameters. Never embed server credentials or deployment logic directly in the Jenkinsfile. Instead, store deployment recipes in your repository and reference them:
// In Jenkinsfile deploy stage
sh '''
./vendor/bin/dep deploy production \
--branch=${BRANCH_NAME} \
--tag=${BUILD_TAG} \
-o keep_releases=5 \
--ansi
''' This approach gives you automatic rollbacks via dep rollback if health checks fail post-deployment. Keep at least 3-5 previous releases on disk so rollbacks are instant filesystem operations rather than full redeployments. For projects requiring database migrations, run them in a pre-deploy hook with maintenance mode enabled, or use backward-compatible migration patterns that allow both old and new code to function during the transition window.
How does Jenkins compare to GitLab CI for PHP projects in 2026?
Choosing between Jenkins and GitLab CI is often the first decision when you build a CI/CD pipeline with Jenkins or consider alternatives. Having shipped production systems with both, the right choice depends entirely on your team size, infrastructure tolerance, and integration requirements. Neither is universally superior.
| Criteria | Jenkins | GitLab CI |
|---|---|---|
| Setup Complexity | High — requires server provisioning, plugin management, and ongoing maintenance | Low — included with GitLab, minimal configuration for basic pipelines |
| Flexibility | Unlimited — plugins for virtually any tool, custom Groovy scripting, complex workflows | Moderate — YAML-based, good coverage but limited extensibility beyond runners |
| Cost (Self-hosted) | Free software + server costs (~Rs 8,000-15,000/month for adequate EC2) | Free tier available + runner costs; Premium features require paid license |
| Learning Curve | Steep — Groovy, plugin ecosystem, security model, distributed builds | Gentle — YAML syntax, well-documented, integrated with repository UI |
| Best For | Complex multi-repo workflows, legacy integrations, enterprise compliance | Single-repo projects, teams already using GitLab, rapid iteration |
| Maintenance Burden | Significant — weekly updates, plugin compatibility, security patches | Minimal — managed by GitLab, automatic runner scaling available |
For solo developers or small agencies in Nepal managing 1-5 client sites, GitLab CI typically delivers faster time-to-value with lower operational overhead. The integrated merge request pipelines, container registry, and environment management cover 90% of use cases without additional tooling. However, when you're integrating with legacy banking APIs, orchestrating deployments across heterogeneous infrastructure, or need fine-grained RBAC for compliance-heavy legal-tech platforms, Jenkins' extensibility justifies its complexity tax.
I've migrated several projects from Jenkins to GitLab CI as teams grew tired of maintenance overhead, and conversely moved projects to Jenkins when GitLab's YAML limitations became blockers. Evaluate honestly against your actual constraints rather than theoretical capabilities. If you're exploring backend architecture decisions alongside CI/CD, understanding Laravel API design patterns helps inform what your pipeline needs to validate and deploy effectively.
Build a CI/CD Pipeline with Jenkins That Actually Ships
Successfully implementing automation requires treating your pipeline as production code with its own testing, documentation, and review process. Start simple with a working end-to-end flow before optimizing parallel stages or adding sophisticated caching. Monitor build times religiously — pipelines exceeding 15 minutes erode developer trust and encourage bypassing CI entirely. Whether you choose Jenkins or an alternative, the goal is reliable, fast feedback that enables confident deployments. If you need hands-on assistance designing or troubleshooting your automation infrastructure, reach out to discuss your specific deployment challenges.

