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.
Ahmed
DevOps & Cloud Engineer
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:
// 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.
// 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:
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.