Jenkins Pipeline: Build, Scan and Push an Image
Take a commit to a scanned, tagged image in a registry, with a gate that blocks rather than reports.
- Time
- 55 min
- Level
- Intermediate
- Objectives
- 4 objectives
- Cost
- Free
Where this fits in the platform
Already built
This lab adds
- A commit that becomes a tagged image in the registry
Before you start
You will need
- Docker
- Jenkins with the Docker Pipeline plugin
- A registry account
You do not need these already — the lab environment below provides them.
You will be able to
- Build a container image from a pipeline without leaking credentials
- Fail a build on a vulnerability rather than logging one
- Tag images so a deployment can be traced to a commit
Cost — Free
— local Jenkins and a free registry account.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
The pipeline builds an image and pushes it as latest. Nobody can say which commit is in production, the scan runs after the push, and the registry password is an environment variable in the job configuration.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
You will need:
- docker
git clone https://github.com/EgyKode/EgyKode-lab.git
cd EgyKode-lab
./egykode start cicd
./egykode shellYou need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
The pipeline
Step 1 of 4
What you are proving: You can produce an image tagged with the commit that built it
Marking this settles success criterion 1.
pipeline {
agent any
environment {
REGISTRY = 'docker.io/waleeddarwesh'
IMAGE = 'egykode-demo'
// Short SHA: traceable, immutable, and sortable by build.
TAG = "${env.GIT_COMMIT.take(7)}"
}
options {
timeout(time: 20, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20'))
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Unit tests') {
steps { sh 'make test || echo "no tests yet"' }
}
stage('Build image') {
steps {
sh 'docker build -t $REGISTRY/$IMAGE:$TAG .'
}
}
stage('Scan image') {
steps {
// --exit-code 1 is what turns a report into a gate.
sh '''
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image \
--exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
$REGISTRY/$IMAGE:$TAG
'''
}
}
stage('Push') {
when { branch 'main' }
steps {
withCredentials([usernamePassword(
credentialsId: 'registry',
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS')]) {
sh '''
echo "$REG_PASS" | docker login $REGISTRY -u "$REG_USER" --password-stdin
docker push $REGISTRY/$IMAGE:$TAG
docker logout $REGISTRY
'''
}
}
}
}
post {
always { sh 'docker image prune -f || true' }
}
}What you are proving: You can keep credentials out of the build log, and say why the latest tag is not what gets deployed
Marking this settles success criteria 3 and 4.
1. Tag with the commit SHA, never latest. latest is not a version — it
is whatever was pushed most recently, so the same manifest deployed twice can
produce two different containers and a rollback has nothing to roll back to.
$TAG ties a running container to exactly one commit.
2. Scan before push, and exit non-zero. A scan after the push has already
published the vulnerable image. --exit-code 1 makes Trivy fail the stage;
--ignore-unfixed removes findings you cannot act on, which is what stops the
gate becoming noise people learn to ignore.
That second flag has a consequence worth knowing before you rely on it: an
end-of-life distribution ships no fixes, so almost everything it carries is
unfixed and --ignore-unfixed discards it. Measured today, debian:10 —
end of life — reports exactly one distinct HIGH under this flag, and
ubuntu:18.04 reports none at all. The most dangerous base images are the ones
this gate says least about, which is why step 3 does not use one.
3. withCredentials, and --password-stdin. The block masks the values in
the log; --password-stdin keeps the password out of the process list, where
ps aux on the agent would otherwise show it.
4. when { branch 'main' }. Feature branches build and scan — the feedback
a developer needs — but only main publishes.
What you are proving: You can prove the vulnerability gate stops a build, using an image you know is old
Marking this settles success criterion 2.
Use a frozen point release of a supported distribution, not an end-of-life one:
FROM debian:12.5-slim # a snapshot Debian has since shipped fixes for
RUN apt-get update && apt-get install -y curlRun the pipeline. The scan stage should fail and the push stage should never execute. A gate you have not seen fail is a gate you cannot trust — this is the only way to know it is wired up.
Then fix it, by moving to the tag that still rebuilds:
FROM debian:12-slim
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*Measured today: debian:12.5-slim reports 15 distinct HIGH or CRITICAL
findings and exits 1; debian:12-slim reports 0 and exits 0. Same
distribution, same package set — the only difference is that one tag is a
snapshot nobody rebuilds and the other is rebuilt as fixes land.
That is the lesson underneath the exercise. A base image is a frozen point
in time. Nothing about it improves on its own, and the gap between it and the
fixes that exist only widens. docker build --pull is not housekeeping.
What you are proving: You can order stages so the cheapest check fails first
This step settles no success criterion on its own.
Unit tests before the image build; the image build before the scan. A test suite that fails in 40 seconds should not run after a six-minute build. Engineers learn about a broken test in under a minute, and the expensive stages only run on code that has earned them.
docker: permission denied in the pipeline
The Jenkins user cannot reach the Docker socket. Add it to the docker group, or mount the socket with correct permissions.
The credential appears in the log
Something echoed it outside withCredentials, or the shell traced it. Avoid set -x in stages that touch secrets.
Trivy reports nothing on a knowingly old image
The database failed to download and it exited 0. Check the stage output for a DB error — a scanner that cannot update is not a gate.
GIT_COMMIT is null
checkout scm has not run yet, or the job is not backed by SCM. Compute the tag after checkout.
Clean up#
Run this even if you did not finish.
docker image prune -af
docker logout docker.ioCost of this lab: Free — local Jenkins and a free registry account.
Maintained by others, on Killercoda. Useful for extra repetition on one tool — it does not complete this lab or settle any criterion above.
- Trivy scenariosmore on image scanning
Success criteria
0 of 4
The concept behind it
Next up
Lab 45 of 59 on the project path