Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Build a CI/CD Pipeline with Jenkins: Practical Tutorial

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 "";
    }
}
Internet UserHTTPS :443Nginx ProxySSL TerminationRate LimitingIP WhitelistJenkins ControllerLocalhost OnlyPort 8080No Direct Access
Secure Jenkins deployment topology: Nginx handles all external traffic while Jenkins listens only on localhost

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.

Monolithic Controller BuildPHP 8.2PHP 8.4Node 22⚠ Dependency Conflicts⚠ Persistent State Drift⚠ Security ExposureDocker Agent BuildsContainer AContainer BContainer C✓ Isolated Environments✓ Reproducible Builds✓ Ephemeral & Secure
Monolithic controller builds risk dependency conflicts while Docker agents provide isolated, reproducible environments

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

  1. 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.
  2. 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.
  3. 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.
  4. 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
'''
Release 2026081701Previous Activevendor/ + public/build/Symlink RemovedRelease 2026081702New Atomic Releasevendor/ + public/build/Symlink ActiveShared Resources.env (persistent)storage/ (logs/cache)uploads/ (user files)Atomic Symlink Swapln -sfn new_release current → Instant SwitchPHP-FPM Reload → OPcache Cleared
Zero-downtime deployment uses atomic symlink swaps to switch traffic instantly while preserving shared resources

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.

CriteriaJenkinsGitLab CI
Setup ComplexityHigh — requires server provisioning, plugin management, and ongoing maintenanceLow — included with GitLab, minimal configuration for basic pipelines
FlexibilityUnlimited — plugins for virtually any tool, custom Groovy scripting, complex workflowsModerate — 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 CurveSteep — Groovy, plugin ecosystem, security model, distributed buildsGentle — YAML syntax, well-documented, integrated with repository UI
Best ForComplex multi-repo workflows, legacy integrations, enterprise complianceSingle-repo projects, teams already using GitLab, rapid iteration
Maintenance BurdenSignificant — weekly updates, plugin compatibility, security patchesMinimal — 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.

Frequently Asked Questions

A single-controller Jenkins setup requires at least 4GB RAM, 2 CPU cores, and 50GB SSD storage for the controller node running Java 17 or 21 LTS. For PHP/Laravel projects with parallel testing, allocate separate agent nodes with 8GB RAM each to prevent build contention and memory exhaustion during Composer installs and PHPUnit runs.

Add the official Jenkins apt repository and GPG key, then install via apt install jenkins. Configure UFW to allow port 8080 only from trusted IPs or reverse proxy through Nginx with SSL termination. Never expose Jenkins directly to the public internet without authentication and HTTPS, as unauthenticated instances are frequently compromised within hours of deployment.

Yes, Jenkins core and most plugins are open-source under MIT/Apache licenses with zero licensing fees. Costs arise from infrastructure hosting, typically Rs 3,000–8,000 monthly (~USD 22–60) for a VPS in Nepal or AWS t3.medium, plus engineering time for maintenance, security patching, and pipeline debugging that managed CI services bundle into their pricing.

GitLab CI offers tighter repository integration, simpler YAML syntax, and hosted runners requiring zero server maintenance, making it preferable for teams already using GitLab. Jenkins excels when you need complex multi-stage workflows, extensive plugin ecosystems, self-hosted agents on existing infrastructure, or integration with legacy systems where GitLab's opinionated pipeline model becomes restrictive for custom deployment logic.

Install multiple PHP versions using Ondřej Surý's PPA and configure distinct Jenkins agent nodes or tool installations for each version. In your Jenkinsfile, use the tools directive or sh steps to select the appropriate PHP binary path per stage. This prevents version conflicts when maintaining Laravel 11 and 12 applications simultaneously on shared infrastructure.

Never commit .env files to version control. Use Jenkins Credentials Binding plugin to inject secrets as environment variables during build stages, or store encrypted configuration in Vault/AWS Secrets Manager and fetch at runtime. For Laravel deployments, maintain a .env.production template in the repo and overlay credentials via Deployer or Ansible during the release phase to avoid exposing sensitive keys in build logs.

The jenkins user lacks write permissions to the deployment directory. Fix by adding jenkins to the www-data group via usermod -aG www-data jenkins, then set directory ownership to www-data:www-data with 2775 permissions so new files inherit the group. Alternatively, use Deployer with SSH key-based authentication to deploy as the application user rather than granting jenkins direct filesystem access.

Enable Composer caching by mounting a persistent volume to /home/jenkins/.composer/cache across builds, or use the Satis private package mirror for internal dependencies. Run composer install --no-dev --prefer-dist --optimize-autoloader in production stages to skip dev dependencies and source downloads. On a typical Laravel project, this reduces dependency installation from three minutes to under forty seconds on subsequent builds.

Yes, integrate Deployer 7 into your Jenkinsfile post-test stage to execute atomic symlinked deployments. Configure shared directories for storage and .env, run php artisan migrate --force within the release path before symlink swap, and reload PHP-FPM via systemctl reload php8.4-fpm after activation. This pattern ensures users never encounter broken states during deployment, matching the workflow I use across multiple production legal-tech portals.

Enable matrix-based authorization, disable signup, enforce strong passwords or SSO via OAuth/LDAP, and restrict script console access to administrators. Regularly update Jenkins core and plugins through the built-in manager, subscribe to the Jenkins security advisory mailing list, and audit installed plugins quarterly. Remove unused plugins entirely, as each additional plugin increases attack surface and maintenance burden without providing value.

Node.js processes often exceed default memory limits or spawn child processes that don't terminate cleanly. Set NODE_OPTIONS=--max-old-space-size=4096 in the build environment, ensure package.json scripts include proper exit codes, and add timeout directives in your Jenkinsfile to kill stalled stages after ten minutes. On resource-constrained agents, move frontend asset compilation to dedicated nodes to prevent blocking backend test execution.

Configure webhook endpoints in your Git provider pointing to JENKINS_URL/generic-webhook-trigger/invoke with token authentication, or use the GitHub/GitLab branch source plugin for native integration. For self-hosted Git servers, poll SCM every two minutes as fallback. Webhooks provide near-instant triggers without polling overhead, but require Jenkins to be reachable from the Git server via HTTPS with valid certificates.

Run migrations during deployment via Deployer or release scripts, not in Jenkins build stages. Builds should produce immutable artifacts tested against fixture databases, while migrations execute once against production data during the release window with rollback capability. Running migrations in CI risks schema drift between environments and makes builds non-reproducible when database state changes independently of code commits.

Enable timestamps and verbose logging in Jenkinsfile options, archive test reports and build artifacts as post-actions regardless of success, and use the Replay feature to rerun modified pipelines without committing fixes. For intermittent failures, add diagnostic sh steps capturing environment variables, disk space, and process lists before critical commands. Console output alone rarely reveals root causes; structured logs and preserved artifacts accelerate troubleshooting significantly.

Monitor JVM heap usage, build queue length, agent availability, and disk space on both controller and agent nodes using Prometheus with the Jenkins metrics plugin. Alert when build duration exceeds baseline thresholds, queue depth persists beyond five minutes, or disk usage crosses eighty percent. Without proactive monitoring, Jenkins silently degrades until builds fail catastrophically during critical deployment windows, leaving teams debugging infrastructure instead of shipping features.

Share this article

Quick Contact Options
Choose how you want to connect me: