Part 2: How to build CI/CD with Jenkins & Snyk


๐Ÿš€ Introduction

๐Ÿ”„ In Part 1 of this series, we built a solid CI/CD pipeline using Jenkins, SonarQube, and Trivy to automate build, test, and security scanning.

๐Ÿ”ฅ In this part, weโ€™re shifting gears from basic automation to production-grade CI/CD by introducing:

  • โฑ๏ธ Parallelized Jenkins pipeline stages to speed up builds
  • ๐Ÿณ Multistage Dockerfiles to reduce image size and improve performance
  • ๐Ÿ›ก๏ธ Snyk Integration for comprehensive dependency and image vulnerability scanning

Letโ€™s elevate your DevOps workflow to the next level!


โš™๏ธ 1. Pipeline Optimization with Parallel Stages

By default, Jenkins runs everything sequentially. But why wait?

We can install dependencies, run Snyk scans, and build Docker images in parallel using the parallel {} block.

โœ… Example: Parallel Dependency Installation

stage('Install Dependencies') {
  steps {
    parallel (
      backend: {
        dir('backend') {
          sh 'npm install'
        }
      },
      frontend: {
        dir('frontend') {
          sh 'npm install'
        }
      }
    )
  }
}

๐Ÿ“‰ This instantly reduced my pipeline time by 30โ€“40%.


๐Ÿณ 2. Build Lean Images with Multistage Dockerfiles

Large Docker images = security risk + slow deploys.

๐Ÿš€ Before: Single-stage Dockerfile (bloated)

# Use a specific version of node
FROM node:21

# Set the working directory
WORKDIR /app

# Copy package.json and package-lock.json first to leverage Docker cache
COPY package*.json ./

# Install dependencies
RUN npm install

# Now copy the rest of your application code
COPY . .

# Use a non-root user to run your container for better security
RUN adduser --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

# Expose the port your app runs on
EXPOSE 5000

# Command to start your application
CMD ["npm", "start"]

โœ… After: Multistage Dockerfile (lean, clean)

# =====================
# Stage 1: Build stage
# =====================
FROM node:21 as build

WORKDIR /app

# Copy only necessary files to install dependencies
COPY package*.json ./

# Install all dependencies (including dev)
RUN npm install

# Copy the rest of the application code
COPY . .

# ==========================
# Stage 2: Production stage
# ==========================
FROM node:21-slim

WORKDIR /app

# Copy only necessary files from the build stage
COPY --from=build /app ./

# Install only production dependencies
RUN npm install --omit=dev

# Copy environment configuration file (optional if used during build)
#COPY .env.sample .env

# Create non-root user for better security
RUN adduser --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

EXPOSE 5000

CMD ["npm", "start"]

โฑ๏ธ Build faster, deploy lighter, and reduce image size by ~60%.


Current Frontend Dockerfile

# Use a specific version of node
FROM node:21

# Set the working directory
WORKDIR /app

# Copy only the necessary files first (e.g., package.json) to leverage Docker cache
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy the rest of your application code
COPY . .

# Use a non-root user to run your container
RUN adduser --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

# Expose the port the app runs on
EXPOSE 5173

# Command to run your application
CMD ["npm", "run", "dev", "--", "--host"]

After Multistage build Dockerfile look like

# -------- Stage 1: Dependencies --------
FROM node:21-alpine AS deps
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --include=dev --prefer-offline --no-audit

# -------- Stage 2: Runtime (Development Server) --------
FROM node:21-alpine AS runtime
WORKDIR /app

# Copy package files
COPY package*.json ./

# Copy node_modules from deps stage
COPY --from=deps /app/node_modules ./node_modules

# Copy ALL source files (maintains exact same structure)
COPY . .

# Set permissions
RUN chown -R node:node /app
USER node

EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]

Measuring Build & Scan Time (Before Optimizations)

Before jumping into parallel execution and multi-stage Docker builds, it’s smart to measure baseline performance. This helps you quantify improvements.


๐Ÿ“Š Track & Visualize Jenkins Build Duration

โœ… 1. Pipeline Stage View Plugin

Best For: Visualizing per-stage execution time directly in the Jenkins UI.

Why It’s Useful:

  • ๐Ÿ“ˆ Clearly displays how long each pipeline stage takes.
  • ๐Ÿ” Makes it easy to identify slow stages (e.g., security scans or image builds).
  • ๐Ÿง‘โ€๐Ÿ’ป Especially handy when experimenting with parallel stages or multi-stage Docker builds.
  • โš™๏ธ No extra configuration required in declarative pipelines โ€” just works out of the box.

๐Ÿ› ๏ธ How to Install:

  1. Navigate to Manage Jenkins โ†’ Plugin Manager
  2. Go to the Available tab and search for:Pipeline: Stage View Plugin
  3. Click Install without Restart.

โฑ๏ธ 1. Check Jenkins Build Duration

  • 1. Check Jenkins Build Duration
    After running a pipeline build, you can quickly analyze how much time each stage took using the Pipeline Stage View plugin.
    ๐Ÿ” Steps to View Stage-Wise Build Time:
    Go to your Jenkins job (e.g., wanderlust-deploy1).
    In the left sidebar, click on “Full Stage View”.
    Youโ€™ll see a graphical view of:
    Each pipeline stage (e.g., Build, Test, Scan, Deploy)
    Time taken by each stage (in seconds/minutes)
    Color-coded status (โœ… Passed, โŒ Failed)

fter applying multistage builds, weโ€™ll check again to see the size reduction.

๐Ÿ›ก๏ธ 3. Replace Trivy with Snyk for Complete Security

While Trivy is great for scanning images, Snyk adds dependency scanning, better dashboards, and Git integration.

๐Ÿ”ง Step-by-Step: Integrate Snyk with Jenkins

๐Ÿง‘โ€๐Ÿ’ป 1๏ธโƒฃ Create a Snyk Account & Retrieve Your API Token

To scan your code and Docker images from Jenkins using Snyk, you’ll need a personal API token from your Snyk account. Here’s how to get it:


๐Ÿ“Œ Step-by-Step:

  1. ๐Ÿ”— Go to https://snyk.io and sign up using your Google account or email.
  2. Once signed in, navigate to the bottom-left corner of your Snyk dashboard.
  3. Click on your profile name/email (e.g., Rajesh K) and select “Account settings” from the dropdown.
    ๐Ÿ“ธ Refer screenshot 1 below.
  1. Youโ€™ll be taken to the Account โ†’ General page.
  2. Scroll down to the โ€œAuth Tokenโ€ section.
    Click on the button that says โ€œclick to showโ€ under the KEY field.
    ๐Ÿ“ธ Refer screenshot 2 below.
  1. Once clicked, it will reveal your Snyk API Token โ€” a long alphanumeric key.
    ๐Ÿ“ธ Refer screenshot 3 below.
  1. โœ… Copy this token โ€” it will be used to configure Jenkins.

โœ… Make sure you treat this token like a password. It authenticates your CLI and Jenkins integration with Snyk.

โš™๏ธ 2. Install Snyk Plugin

  • ๐Ÿงฉ Step A: Install the Snyk Security Plugin
  • Open your Jenkins dashboard and navigate to:
    Manage Jenkins โ†’ Plugin Manager
  • Click the Available tab.
  • Search for โ€œSnyk Security Pluginโ€
  • Check the box and click Install without restart
  • ๐Ÿ“ฆ This plugin allows Jenkins to run Snyk scans natively in pipeline stages using the snykSecurity block.

๐Ÿ” Step B: Add Your Snyk API Token as a Credential

  1. Go to Manage Jenkins โ†’ Credentials โ†’ (Global) โ†’ Add Credentials
  2. Set the Kind to: โ€œSnyk API Tokenโ€ (๐Ÿ“Œ Not โ€œSecret Textโ€)
  3. Enter the following:
    • ID: snyk-api
    • Description: Snyk API Token for CI/CD Scans
    • Token: Paste your API token from the Snyk dashboard
  4. Click Create

โœ… With this, your Jenkins instance is now securely integrated with Snyk and ready to scan your source code and Docker images.

๐Ÿงฐ 3๏ธโƒฃ Configure the Snyk CLI Tool in Jenkins (One-Time Setup)

This step ensures Jenkins downloads and uses the Snyk CLI automatically when your pipeline runs.

๐Ÿ“ Where to Configure:

Navigate to:
Jenkins โ†’ Manage Jenkins โ†’ Global Tool Configuration โ†’ Snyk installations

๐Ÿ› ๏ธ Configuration:

  • Name: snyk
    (Make sure this matches what you use in snykSecurity(snykInstallation: 'snyk', ...))
  • โœ… Check: โ€œInstall automaticallyโ€
  • Version: latest
  • Update policy interval: 24
  • OS platform architecture: Auto-detection (default and recommended)

โœ… Why This Matters:

Ensures compatibility with the snykSecurity pipeline steps

No manual CLI installation needed on Jenkins agents

Always uses the latest version of the Snyk CLI

โœ… 4. Use in Jenkinsfile

snykSecurity(
  snykInstallation: 'snyk',
  snykTokenId: 'snyk-api',
  targetFile: 'package.json',
  projectName: 'my-backend-src',
  failOnIssues: false,
  additionalArguments: "--severity-threshold=high"
)

Stage: Snyk Scanning (Parallel Block)

This stage runs four Snyk scans in parallel, significantly improving performance by reducing total pipeline time.

โœ… 1. Frontend Dependency Scan

dir('wanderlust-3tier-project/frontend') {
    snykSecurity(
        snykInstallation: 'snyk',
        snykTokenId: 'snyk-api',
        targetFile: 'package.json',
        projectName: 'wanderlust-frontend-src',
        failOnIssues: false,
        additionalArguments: "--severity-threshold=critical"
    )
}

Purpose: Scans package.json in the frontend directory for vulnerable open-source dependencies.

FailOnIssues: false to avoid breaking the build if issues are found.

Threshold: Only fails on critical vulnerabilities (if failOnIssues were true).

projectName: Helps uniquely identify the scan in the Snyk dashboard.

โœ… 2. Backend Dependency Scan

dir('wanderlust-3tier-project/backend') {
    snykSecurity(
        snykInstallation: 'snyk',
        snykTokenId: 'snyk-api',
        targetFile: 'package.json',
        projectName: 'wanderlust-backend-src',
        failOnIssues: false,
        additionalArguments: "--severity-threshold=critical"
    )
}

Same logic as the frontend scan, but scoped to the backend source dependencies.

๐Ÿณ 3. Backend Image Scan

snykSecurity(
    snykInstallation: 'snyk',
    snykTokenId: 'synk-api',
    failOnIssues: false,
    projectName: 'wanderlust-backend-image',
    additionalArguments: "--docker ${BACKEND_IMAGE}:${IMAGE_TAG} --severity-threshold=critical"
)

Scans the built Docker image of the backend (rjshk013/wanderlust-backend:<tag>).

Checks for vulnerabilities in the OS layer and image dependencies.

--severity-threshold=critical filters out less severe issues.


๐Ÿณ 4. Frontend Image Scan

snykSecurity(
    snykInstallation: 'snyk',
    snykTokenId: 'snyk-api',
    failOnIssues: false,
    projectName: 'wanderlust-frontend-image',
    additionalArguments: "--docker ${FRONTEND_IMAGE}:${IMAGE_TAG} --severity-threshold=critical"
)

Here is the final full Pipeline script

pipeline {
    agent { label 'agent' }

    tools {
        jdk 'jdk21'
        nodejs 'node21'
    }

    environment {
        IMAGE_TAG = "${BUILD_NUMBER}"
        BACKEND_IMAGE = "rjshk013/wanderlust-backend"
        FRONTEND_IMAGE = "rjshk013/wanderlust-frontend"
    }

    stages {

        stage('SCM Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/rjshk013/devops-projects.git'
            }
        }

        stage('Install Dependencies') {
            steps {
                parallel (
                    backend: {
                        dir('wanderlust-3tier-project/backend') {
                            sh "npm install || true"
                        }
                    },
                    frontend: {
                        dir('wanderlust-3tier-project/frontend') {
                            sh "npm install"
                        }
                    }
                )
            }
        }

        stage('Docker Build') {
            steps {
                dir('wanderlust-3tier-project') {
                    sh '''
docker build -f Dockerfile_multi-stage -t ${BACKEND_IMAGE}:${IMAGE_TAG} ./backend
docker build -f Dockerfile_multi-stage -t ${FRONTEND_IMAGE}:${IMAGE_TAG} ./frontend
                    '''
                }
            }
        }

        stage('Snyk Scanning') {
            parallel {

                stage('Frontend Dependency Scan') {
                    steps {
                        dir('wanderlust-3tier-project/frontend') {
                            snykSecurity(
                                snykInstallation: 'snyk',
                                snykTokenId: 'snyk-api',
                                targetFile: 'package.json',
                                projectName: 'wanderlust-frontend-src',
                                failOnIssues: false,
                                additionalArguments: "--severity-threshold=critical"
                            )
                        }
                    }
                }

                stage('Backend Dependency Scan') {
                    steps {
                        dir('wanderlust-3tier-project/backend') {
                            snykSecurity(
                                snykInstallation: 'snyk',
                                snykTokenId: 'snyk-api',
                                targetFile: 'package.json',
                                projectName: 'wanderlust-backend-src',
                                failOnIssues: false,
                                additionalArguments: "--severity-threshold=critical"
                            )
                        }
                    }
                }

                stage('Backend Image Scan') {
                    steps {
                        snykSecurity(
                            snykInstallation: 'snyk',
                            snykTokenId: 'snyk-api',
                            failOnIssues: false,
                            projectName: 'wanderlust-backend-image',
                            additionalArguments: "--docker ${BACKEND_IMAGE}:${IMAGE_TAG} --severity-threshold=critical"
                        )
                    }
                }

                stage('Frontend Image Scan') {
                    steps {
                        snykSecurity(
                            snykInstallation: 'snyk',
                            snykTokenId: 'snyk-api',
                            failOnIssues: false,
                            projectName: 'wanderlust-frontend-image',
                            additionalArguments: "--docker ${FRONTEND_IMAGE}:${IMAGE_TAG} --severity-threshold=critical"
                        )
                    }
                }
            }
        }

        stage('Run SonarQube') {
            environment {
                scannerHome = tool 'sonar-scanner'
            }
            steps {
                withSonarQubeEnv('sonar-server') {
                    sh """
                        ${scannerHome}/bin/sonar-scanner \
                        -Dsonar.projectKey=blog-app \
                        -Dsonar.projectName=blog-app \
                        -Dsonar.sources=wanderlust-3tier-project
                    """
                }
            }
        }

        stage('Push to Docker Hub') {
            steps {
                withCredentials([string(credentialsId: 'docker-hub-token', variable: 'DOCKER_TOKEN')]) {
                    sh '''
                        echo "${DOCKER_TOKEN}" | docker login -u rjshk013 --password-stdin
                        docker push ${BACKEND_IMAGE}:${IMAGE_TAG}
                        docker push ${FRONTEND_IMAGE}:${IMAGE_TAG}
                    '''
                }
            }
        }

        stage('Remote Deploy on EC2 Host with Docker Compose') {
            steps {
                sshagent(credentials: ['deploy-server-ssh']) {
                    sh '''
                        echo "๐Ÿš€ Deploying on EC2 instance with docker compose..."
                        ssh -o StrictHostKeyChecking=no user@172.18.0.1 '
                            cd /home/user/devops-projects/wanderlust-3tier-project &&
                            docker compose pull &&
                            docker compose up -d
                        '
                    '''
                }
            }
        }
    }


post {
    always {
        echo '๐Ÿ“ฆ Pipeline execution completed'
        deleteDir()
    }
    success {
        echo "โœ… Wanderlust pipeline succeeded: Build #${BUILD_NUMBER}"
    }
    failure {
        echo "โŒ Wanderlust pipeline failed: Build #${BUILD_NUMBER}"
    }
}
}

โœ… Final Pipeline Flow

  • ๐Ÿ”€ Parallel install and scan stages
  • ๐Ÿณ Multistage build for backend & frontend
  • ๐Ÿ›ก๏ธ Snyk scan for both code & containers


๐Ÿณ Docker Image Size After Multistage Build

After applying multistage builds to both frontend and backend Dockerfiles, we checked the image size using below command on our host machine or jenkins agent container

docker images

๐Ÿ“‰ Result:
The image size has been significantly reduced, confirming the effectiveness of multistage optimization by eliminating unnecessary dependencies and build artifacts from the final image.

๐Ÿ” Snyk Dashboard Insights After CI/CD Run

Once our Jenkins pipeline completes the dependency and image scans for both backend and frontend using the Snyk Security Plugin, the vulnerabilities are automatically reflected in the Snyk dashboard:

๐Ÿ“Š Project Segmentation
The dashboard intelligently groups the scanned artifacts:

  • wanderlust-backend-image
  • wanderlust-frontend-image
  • Path-based:
    • /app/package.json
    • /usr/local/lib/node_modules
    • other specific build locations (like esbuild)

๐Ÿ“Œ Key Highlights from the Report:

๐Ÿ“ wanderlust-backend-image

  • 1 Critical, 7 High, 10 Medium, 42 Low severity vulnerabilities
  • Deep analysis of Docker layers, package.json, and node_modules directories

๐Ÿ“ wanderlust-frontend-image

  • Similar breakdown: 1 Critical, 5 High, 10 Medium, 40 Low
  • Even detects vulnerabilities in build-time binaries (e.g., esbuild)

๐Ÿงฉ Source Code Scan

  • Source repositories like wanderlust-backend-src and wanderlust-frontend-src scanned via package.json
  • Backend source has 6 high-severity issues; frontend source has 3

๐Ÿ”ฎ What’s Coming in Part 3?

In the next blog post, Iโ€™ll cover:

  • ๐Ÿ“ˆ Visual build metrics using dashboards for performance insights
    ๐Ÿ”” Slack and email notifications to keep teams informed in real time
    ๐Ÿณ Advanced Docker optimizations using layer caching to reduce build time
    ๐Ÿ›ก๏ธ Security and performance best practices for production-grade pipelines

๐Ÿ”— Part 1 Recap: How I built a CI/CD pipeline with Jenkins, SonarQube & Trivy

๐Ÿ’ฌ Drop your thoughts or ask questions in the comments โ€” and follow for Part 3!


One thought on “Part 2: How to build CI/CD with Jenkins & Snyk

Leave a Comment

Your email address will not be published. Required fields are marked *

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.