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.

Jenkins Declarative Pipeline Tutorial

By Kokil Thapa | Last reviewed: August 2026

Most teams adopting continuous integration get stuck on syntax errors and opaque failures before shipping a single build. This Jenkins Declarative Pipeline tutorial cuts through the documentation noise to show you exactly how to structure reliable, maintainable pipelines for PHP and Laravel applications in 2026. If you are maintaining legacy scripts or setting up automation for the first time, understanding the strict declarative syntax is the fastest path to stable deployments.

While I primarily use GitLab CI and Deployer 7 for my own legal-tech and eCommerce projects, many enterprise clients and larger Nepali software houses standardize on Jenkins for its plugin ecosystem and granular access control. When integrating with these environments or migrating legacy systems, knowing the declarative model is non-negotiable. For developers exploring broader automation strategies beyond Jenkins, understanding CI/CD pipeline setup expert practices in Nepal provides essential context for choosing the right tool for your specific infrastructure constraints.

What is the correct structure for a Jenkins Declarative Pipeline tutorial?

The declarative model enforces a strict hierarchy that prevents the "spaghetti code" common in older scripted pipelines. Every valid pipeline must be wrapped in a pipeline {} block containing an agent directive and at least one stage. This rigidity is a feature, not a limitation; it enables Jenkins to parse the entire workflow before execution, providing syntax validation and visualization that scripted pipelines cannot match.

Declarative Pipeline Hierarchypipeline { }agent { any }environment { }options { }stages { }stage('Build') { }steps { sh '...' }post { always {...} }stage('Test') { }parallel { ... }when { branch 'main' }stage('Deploy') { }input { message '...' }agent { label 'prod' }
Structural hierarchy of a Jenkins Declarative Pipeline showing required blocks and nesting rules

In practice, the most common mistake I see in junior developers' pipelines is placing directives like environment or options inside steps blocks. These are section-level directives and must be siblings to stages, not children of executable steps. The agent directive is mandatory at the top level; without it, Jenkins has nowhere to execute your workspace checkout or initial commands. You can override this globally-defined agent per-stage if specific tasks require different hardware or Docker images, but the top-level declaration remains required for syntactic validity.

Essential Directives Reference

  • agent: Defines where the pipeline executes. Use any for simple setups, docker for containerized builds, or label for specific node targeting.
  • environment: Sets variables available to all stages. Ideal for API keys (via credentials binding), version tags, and database connection strings.
  • options: Configures pipeline-level behavior including timeout, retry, disableConcurrentBuilds, and timestamps.
  • triggers: Automates execution via cron, pollSCM, or webhook integrations. Essential for nightly regression tests.
  • parameters: Enables manual input for deployment targets, feature flags, or version overrides directly from the Jenkins UI.

How do you configure agents and Docker in a Jenkins Declarative Pipeline tutorial?

Agent configuration determines both security isolation and reproducibility. For PHP and Laravel projects in 2026, running builds inside Docker containers is effectively mandatory. Host-based agents accumulate stale dependencies, conflicting PHP versions, and permission drift that causes "works on my machine" failures. Containerized agents guarantee every build starts from a known state.

pipeline {
    agent {
        docker {
            image 'php:8.4-cli'
            args '-v /var/run/docker.sock:/var/run/docker.sock'
        }
    }
    
    environment {
        COMPOSER_CACHE_DIR = '/tmp/composer-cache'
        APP_ENV = 'testing'
    }
    
    stages {
        stage('Install Dependencies') {
            steps {
                sh 'composer install --no-interaction --prefer-dist'
            }
        }
        
        stage('Run Tests') {
            agent {
                docker {
                    image 'php:8.4-cli'
                    args '--network host -e DB_HOST=mysql-test'
                }
            }
            steps {
                sh 'php artisan test --parallel'
            }
        }
    }
}

The example above demonstrates agent inheritance and override. The top-level agent uses a standard PHP 8.4 CLI image for dependency installation, leveraging Composer's cache volume to avoid redundant downloads. The test stage overrides this with network access to a separate MySQL container. This pattern avoids installing database clients in your application image while maintaining test isolation.

When working with Laravel applications specifically, ensure your Docker image includes required extensions (bcmath, gd, pdo_mysql, redis). Building a custom image cached in your organization's registry saves significant time compared to installing extensions on every build. For teams managing multiple PHP applications, understanding Laravel developer best practices in Nepal helps align CI configurations with local development standards and team skill sets.

Docker Agent Gotchas

  1. Volume Mounts: Always mount Composer/npm cache directories. Without caching, dependency resolution adds 2-5 minutes per build.
  2. User Permissions: Docker runs as root by default. Add --user 1000:1000 to args or configure USER in Dockerfile to prevent artifact permission issues on shared volumes.
  3. Docker-in-Docker: Avoid DinD unless absolutely necessary. Mount the host socket (/var/run/docker.sock) for building/pushing images instead.
  4. Cleanup: Add post { always { sh 'docker system prune -f' } } to prevent disk exhaustion on long-running agents.

How do you implement parallel stages and conditional execution?

Sequential execution wastes resources when independent tasks exist. Laravel projects typically have unit tests, feature tests, static analysis, and frontend builds that can run simultaneously. Parallel stages reduce total pipeline duration by 40-60% on typical PHP applications.

Sequential (12 min)Unit Tests (3m)Feature Tests (5m)Static Analysis (2m)Frontend Build (2m)Parallel (5 min)Unit Tests (3m)Feature Tests (5m)Static Analysis (2m)Frontend Build (2m)Deploy (gated)
Parallel execution reduces Laravel CI pipeline duration from 12 minutes to 5 minutes by running independent test suites concurrently
stage('Quality Gates') {
    parallel {
        stage('Unit Tests') {
            steps {
                sh 'php artisan test --testsuite=Unit'
            }
        }
        stage('Feature Tests') {
            steps {
                sh 'php artisan test --testsuite=Feature --parallel'
            }
        }
        stage('PHPStan Analysis') {
            steps {
                sh './vendor/bin/phpstan analyse --memory-limit=1G'
            }
        }
        stage('Vite Build') {
            agent {
                docker { image 'node:22-alpine' }
            }
            steps {
                sh 'npm ci && npm run build'
            }
        }
    }
}

Conditional execution via when directives prevents unnecessary work. Feature branches rarely need full deployment pipelines, and main branch builds shouldn't run experimental test suites. The when block evaluates before entering a stage, skipping it entirely if conditions fail.

Common Conditional Patterns

  • when { branch 'main' } — Restrict deployment to protected branches only.
  • when { changeset '**/*.php' } — Skip backend tests when only frontend assets changed.
  • when { expression { return params.DEPLOY_TARGET == 'production' } } — Gate production deploys behind parameter selection.
  • when { not { triggeredBy 'TimerTrigger' } } — Differentiate scheduled vs. commit-triggered behavior.

How does Jenkins Declarative Pipeline compare to Scripted Pipeline in 2026?

The choice between declarative and scripted pipelines affects maintainability, onboarding speed, and long-term operational burden. While scripted pipelines offer unlimited Groovy flexibility, declarative pipelines provide structure that pays dividends as team size and project complexity grow.

CriteriaDeclarative PipelineScripted Pipeline
Syntax ValidationPre-flight checks catch errors before executionRuntime failures only; no pre-validation
Restart from StageNative support via Blue Ocean / UINot supported; must rerun entire pipeline
Learning CurveLow; structured DSL with limited GroovyHigh; requires full Groovy knowledge
Complex LogicLimited; use shared libraries for advanced casesUnlimited; arbitrary Groovy code allowed
VisualizationExcellent Blue Ocean / Stage View integrationPoor; linear console output only
MaintainabilityHigh; consistent structure across projectsVariable; depends on author discipline
Best ForStandard CI/CD, Laravel/PHP apps, teamsComplex orchestration, legacy migrations

In 2026, declarative should be your default. Scripted pipelines remain relevant only when you need dynamic stage generation based on runtime data or complex conditional branching that exceeds when directive capabilities. Even then, wrap scripted logic in shared libraries and call them from declarative pipelines rather than writing entire scripted workflows.

How do you handle secrets and post-build actions securely?

Hardcoding credentials in Jenkinsfiles is a critical security failure. Jenkins provides credential binding that injects secrets as environment variables only during execution, never persisting them in logs or artifacts. For Laravel projects, this typically means database passwords, API keys for payment gateways like eSewa or Khalti, and deployment tokens.

environment {
    DB_PASSWORD = credentials('laravel-db-password')
    DEPLOY_TOKEN = credentials('deployer-ssh-key')
    AWS_ACCESS_KEY_ID = credentials('aws-access-key')
    AWS_SECRET_ACCESS_KEY = credentials('aws-secret-key')
}

stages {
    stage('Deploy') {
        when { branch 'main' }
        steps {
            sshagent(credentials: ['deployer-ssh-key']) {
                sh './vendor/bin/dep deploy production'
            }
        }
    }
}

post {
    success {
        slackSend color: 'good', message: "✅ ${env.JOB_NAME} #${env.BUILD_NUMBER} deployed successfully"
    }
    failure {
        slackSend color: 'danger', message: "❌ ${env.JOB_NAME} #${env.BUILD_NUMBER} failed. Check console."
        archiveArtifacts artifacts: 'storage/logs/*.log', allowEmptyArchive: true
    }
    always {
        cleanWs()
        junit allowEmptyResults: true, testResults: 'reports/junit.xml'
    }
}

The post section is where declarative pipelines prove their worth over ad-hoc scripting. The always block guarantees cleanup and test result publishing regardless of success or failure. The failure block captures logs automatically for debugging. On production Laravel systems I've maintained, automatic log archival on failure reduced mean-time-to-diagnosis from hours to minutes.

Pipeline Completespost { always }cleanWs(), junit()post { success }slackSend(color: good)archiveArtifacts('dist/')post { failure }slackSend(color: danger)archiveArtifacts('logs/')Build Marked SUCCESSBuild Marked FAILED
Post-build execution flow showing guaranteed always block and conditional success/failure handlers

For teams evaluating whether to invest in Jenkins expertise or outsource pipeline management entirely, comparing costs against hiring a dedicated DevOps engineer in Nepal for website automation often reveals that managed CI/CD or simpler alternatives like GitLab CI deliver better ROI for small-to-medium Laravel shops. Jenkins shines at scale or in regulated environments requiring audit trails and granular permissions.

Practical Next Steps After This Jenkins Declarative Pipeline Tutorial

Start with a minimal working pipeline before optimizing. Create a Jenkinsfile with agent, single stage, and basic post block. Validate syntax using Jenkins' built-in linter (/pipeline-syntax/ endpoint) before committing. Incrementally add parallel stages, conditional execution, and credential bindings as needs emerge rather than designing a perfect pipeline upfront.

Monitor build duration and failure rates for two weeks after initial setup. Parallel stages that frequently fail together should be merged back to sequential execution to reduce debugging overhead. Stages that consistently pass without catching issues are candidates for removal or sampling. Pipeline maintenance is ongoing; treat your Jenkinsfile as production code subject to the same review standards as application logic.

If you're implementing CI/CD for a Laravel application and need hands-on guidance tailored to your infrastructure, reach out to discuss your pipeline requirements. Whether you're migrating from scripted pipelines, integrating with existing Jenkins infrastructure, or evaluating whether Jenkins is the right fit versus GitLab CI or GitHub Actions, practical experience matters more than documentation.

Frequently Asked Questions

A structured DSL using the pipeline block to define CI/CD workflows with strict syntax validation, stages, and built-in error handling.

Declarative enforces structure and offers post-build actions; Scripted allows arbitrary Groovy code but lacks guardrails and readability.

agent, stages, and at least one stage with steps are required; triggers, environment, options, and post are optional.

Store it in the repository root as Jenkinsfile so pipeline definitions version with application code and survive branch changes.

Yes, load them via @Library annotation at the top of the Jenkinsfile or configure global libraries in Manage Jenkins for reuse across projects.

Use credentials() helper within environment blocks referencing Jenkins Credentials Store IDs. Never hardcode tokens or passwords directly in the Jenkinsfile text. In my experience managing deployments for legal-tech portals like Court Marriage In Nepal, binding secrets this way prevents accidental exposure in build logs while keeping configuration portable across staging and production environments without manual intervention during releases.

These occur when omitting agent, stages, or steps blocks. Declarative syntax validates structure before execution unlike Scripted pipelines. Always include an agent directive even if using none and defining agents per-stage. I have encountered this repeatedly when junior developers copy-paste fragments without understanding that every declarative pipeline must declare these three sections explicitly regardless of whether they use Docker containers, Kubernetes pods, or bare-metal executors for their actual build steps.

Nest parallel directive inside a parent stage containing multiple named sub-stages. Each runs concurrently on available executors. Avoid shared workspace writes between parallel branches to prevent race conditions. On eCommerce projects like Petals Nepal where asset compilation and test suites ran simultaneously, we isolated outputs into separate directories then merged artifacts in a subsequent sequential stage to eliminate intermittent build failures caused by concurrent file access conflicts during high-load deployment windows.

Use post blocks for cleanup, notifications, and archiving because they execute regardless of success or failure. Reserve try-catch only inside script blocks for conditional logic recovery. Declarative post provides always, success, failure, changed, and regression conditions natively. I prefer this pattern over wrapping entire stages because it keeps pipeline intent visible at the top level rather than burying critical notification logic deep inside procedural Groovy code that becomes unmaintainable as workflow complexity grows.

Use the Replay feature in Jenkins UI or install the jenkins-cli tool to parse locally. Alternatively add a pre-commit hook running curl against your Jenkins instance’s pipeline-model-converter endpoint. This catches structural errors before pushing. I enforce this on all client projects including Adventure Third Pole Trek because debugging malformed declarative syntax through failed builds wastes executor time and delays feedback loops significantly compared to catching missing braces or invalid directives during local development.

Absolutely. Define stages for composer install, php artisan migrate:fresh --env=testing, vendor/bin/phpunit, and static analysis tools like PHPStan. Cache vendor directory between builds using stash/unstash or persistent volumes. For Laravel 12 applications running on PHP 8.4, I typically isolate database migrations into a dedicated stage that rolls back automatically in post-failure to avoid leaving test databases in dirty states that cause cascading false negatives on subsequent runs.

Enable timestamps and set options { timeout(time: 30, unit: 'MINUTES') } to catch hangs. Check console output for skipped stages indicating earlier conditional failures. Add echo statements before critical steps since Declarative swallows some exceptions silently. On production systems like Notary Nepal, I discovered that uncaught shell exit codes in sh steps appeared successful until adding explicit returnStatus checks revealed underlying permission issues that only manifested intermittently under specific executor load conditions.

Jenkins itself is free open-source software. Costs come from infrastructure hosting the controller and agents. A minimal AWS t3.medium setup runs approximately Rs 4,500 monthly (~USD 34). Factor in storage for build artifacts and backup retention. For Nepal-based teams, self-hosting on local servers often makes more sense than cloud billing unpredictability, especially when pipeline volume stays below fifty daily executions across multiple client projects sharing the same controller instance.

Start by extracting reusable logic into shared libraries, then restructure remaining code into stages with proper agent declarations. Replace node blocks with agent directives and wrap imperative Groovy inside script steps only when absolutely necessary. Expect two to four weeks for complex pipelines based on my experience modernizing legacy CI setups. The migration pays off through improved readability and onboardability even though initial refactoring feels tedious compared to continuing with familiar unstructured scripts.

GitLab CI and GitHub Actions offer similar declarative YAML syntax with tighter repository integration and managed runners eliminating maintenance overhead. However, Jenkins remains relevant when you need granular control over heterogeneous agents, complex approval workflows, or on-premise compliance requirements. For most new Laravel or WooCommerce projects I start today, I evaluate GitLab CI first unless the client already operates Jenkins infrastructure or requires specific plugin integrations unavailable elsewhere in the ecosystem.

Share this article

Quick Contact Options
Choose how you want to connect me: