A

Ahmed

DevOps Engineer · Software Engineer

Egyptahmed@example.comOperational
Back to Blog
CI/CDJenkinsDevOps

Building a Production-Grade CI/CD Pipeline with Jenkins

Step-by-step guide to creating a robust Jenkins pipeline with Docker agents, automated testing, and deployment.

A

Ahmed

Dec 15, 202510 min read

A production CI/CD pipeline is not a luxury — it's the backbone of any engineering team that ships with confidence. Jenkins, despite being one of the older players, remains unmatched in flexibility. Combined with Docker agents and declarative pipelines, you can build a system that is reproducible, scalable, and auditable.

Declarative vs Scripted Pipelines

Jenkins supports two syntaxes. Always prefer Declarative — it's structured, validates at parse time, and is far easier to maintain by the whole team:

groovy
// Jenkinsfile (Declarative)
pipeline {
  agent {
    docker {
      image 'node:20-alpine'
      args '-v /tmp:/tmp'
    }
  }

  environment {
    APP_ENV   = 'production'
    REGISTRY  = 'registry.mycompany.com'
    IMAGE_TAG = "${env.GIT_COMMIT[0..7]}"
  }

  stages {
    stage('Install') {
      steps {
        sh 'npm ci --prefer-offline'
      }
    }

    stage('Lint & Test') {
      parallel {
        stage('Lint') {
          steps { sh 'npm run lint' }
        }
        stage('Unit Tests') {
          steps {
            sh 'npm test -- --coverage'
            junit 'coverage/junit.xml'
          }
        }
      }
    }

    stage('Build') {
      steps {
        sh 'npm run build'
        archiveArtifacts artifacts: 'dist/**', fingerprint: true
      }
    }

    stage('Docker Build & Push') {
      steps {
        withCredentials([usernamePassword(
          credentialsId: 'registry-creds',
          usernameVariable: 'DOCKER_USER',
          passwordVariable: 'DOCKER_PASS'
        )]) {
          sh '''
            echo "$DOCKER_PASS" | docker login $REGISTRY -u "$DOCKER_USER" --password-stdin
            docker build -t $REGISTRY/myapp:$IMAGE_TAG .
            docker push $REGISTRY/myapp:$IMAGE_TAG
          '''
        }
      }
    }

    stage('Deploy to Staging') {
      steps {
        sh 'kubectl set image deployment/myapp myapp=$REGISTRY/myapp:$IMAGE_TAG -n staging'
        sh 'kubectl rollout status deployment/myapp -n staging --timeout=120s'
      }
    }

    stage('Smoke Tests') {
      steps {
        sh 'npm run test:smoke -- --env staging'
      }
    }

    stage('Deploy to Production') {
      when { branch 'main' }
      input { message "Deploy to production?"; ok "Ship it!" }
      steps {
        sh 'kubectl set image deployment/myapp myapp=$REGISTRY/myapp:$IMAGE_TAG -n production'
        sh 'kubectl rollout status deployment/myapp -n production --timeout=300s'
      }
    }
  }

  post {
    success { slackSend color: 'good', message: "✅ Pipeline passed: ${env.JOB_NAME} [${env.BUILD_NUMBER}]" }
    failure { slackSend color: 'danger', message: "❌ Pipeline failed: ${env.JOB_NAME} [${env.BUILD_NUMBER}]" }
    always  { cleanWs() }
  }
}

Docker Agents: Why They Matter

Running builds directly on the Jenkins host is a recipe for dependency conflicts and security issues. Docker agents give each build a clean, isolated environment:

  • Every build starts from a known-good image — no snowflake agents
  • Different pipelines can use different language runtimes simultaneously
  • The build environment is version-controlled alongside the code
  • Agents are ephemeral — no leftover state between builds

Tip

Build your own base agent images with your company's security tools, certificates, and CLI tools pre-installed. Store them in your internal registry to avoid slow pulls from Docker Hub.

Credential Management

Danger

Never hardcode credentials, tokens, or secrets in your Jenkinsfile. Use Jenkins' built-in Credentials Store or integrate with HashiCorp Vault.

groovy
// Fetching secrets from HashiCorp Vault
stage('Deploy') {
  steps {
    withVault(configuration: [vaultUrl: 'https://vault.internal'],
              vaultSecrets: [[path: 'secret/prod/db', secretValues: [
                [envVar: 'DB_PASSWORD', vaultKey: 'password']
              ]]]) {
      sh 'deploy.sh'
    }
  }
}

Parallel Testing for Speed

The single biggest win in any CI pipeline is parallelism. Lint, unit tests, security scans — run them all at the same time. The pipeline duration equals the slowest job, not the sum of all jobs:

groovy
stage('Validate') {
  parallel {
    stage('Unit Tests')    { steps { sh 'make test-unit' } }
    stage('Lint')          { steps { sh 'make lint' } }
    stage('SAST Scan')     { steps { sh 'make snyk-test' } }
    stage('License Check') { steps { sh 'make license-check' } }
  }
}
“

A CI pipeline exists not to catch bugs — that's a nice side effect. It exists to protect your main branch so that any revision in history is always deployable.