Lesson 03 of 8 · Level 1 — Foundations
Build, test and publish images
Turn a commit into a tested, scanned image in a registry: test reports Jenkins can show, rootless image builds on Kubernetes agents with BuildKit or Buildah (not the Docker socket), tags and digests you can trace back to the commit, and handing the release to deployment.
The flow
checkout → unit tests (+ reports) → build image → scan → push (tag = commit SHA) → record digest → hand over to deployment
A bakery line: mix and taste (tests), bake (build), inspect every loaf (scan), wrap and label it with the batch number (tag and digest), then hand it to the delivery van (deployment). Nothing leaves without its label.
Tests Jenkins can show
Publish results so Jenkins can show trends and failures per test, not just "the build is red":
stage('Test') {
steps { sh 'pytest --junitxml=reports/unit.xml' }
post { always { junit 'reports/*.xml' } }
}
Add coverage and static analysis the same way (Coverage and Warnings Next Generation plugins).
Building images without the Docker socket
On Kubernetes agents, don't mount /var/run/docker.sock or run privileged Docker-in-Docker: both hand the build root-level access to the node. Build rootless instead:
| Builder | Notes |
|---|---|
| BuildKit (rootless) | moby/buildkit:rootless image, buildctl-daemonless.sh; same engine as docker buildx |
| Buildah | Builds OCI images from Dockerfiles without a daemon |
| Kaniko | Long popular for this; Google archived the original project in 2025, community forks continue. Check maintenance status before choosing it |
A Kubernetes agent with a BuildKit container (lesson 06 explains pod templates):
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: buildkit
image: moby/buildkit:rootless
command: ["sleep"]
args: ["infinity"]
securityContext:
seccompProfile: { type: Unconfined }
appArmorProfile: { type: Unconfined }
env:
- { name: BUILDKITD_FLAGS, value: "--oci-worker-no-process-sandbox" }
- name: tools
image: aquasec/trivy:latest
command: ["sleep"]
args: ["infinity"]
'''
}
}
environment {
IMAGE = 'registry.example.com/shop/orders-api'
TAG = "${env.GIT_COMMIT.take(12)}"
}
stages {
stage('Build & push') {
steps {
container('buildkit') {
withCredentials([file(credentialsId: 'registry-docker-config', variable: 'DOCKER_CONFIG_FILE')]) {
sh '''
mkdir -p ~/.docker && cp "$DOCKER_CONFIG_FILE" ~/.docker/config.json
buildctl-daemonless.sh build --frontend dockerfile.v0 \
--local context=. --local dockerfile=. \
--output type=image,name=$IMAGE:$TAG,push=true
'''
}
}
}
}
stage('Scan') {
steps { container('tools') { sh 'trivy image --exit-code 1 --severity CRITICAL $IMAGE:$TAG' } }
}
}
}
(The rootless BuildKit image's documentation lists the exact security settings for your Kubernetes version; unconfined seccomp/AppArmor is needed for its user namespaces. Pin image versions instead of latest in real pipelines.)
Tags you can trace
- Tag with the commit SHA (
orders-api:3f1c9a0b7e2d), plus a semantic version on release tags (orders-api:1.4.0). - Record the digest after pushing and deploy by digest where possible.
- Never deploy
latest.
Handing over to deployment
Two models:
| Model | Jenkins does | Trade-off |
|---|---|---|
| Push | kubectl/helm upgrade against the cluster |
Simple; Jenkins holds cluster credentials and can drift from Git |
| GitOps (recommended) | Opens a PR (or commits) bumping the image tag/digest in the config repository | Jenkins never touches clusters; Argo CD/Flux deploy what Git says |
stage('Promote to dev') {
when { branch 'main' }
steps {
withCredentials([sshUserPrivateKey(credentialsId: 'gitops-deploy-key', keyFileVariable: 'KEY')]) {
sh '''
export GIT_SSH_COMMAND="ssh -i $KEY -o StrictHostKeyChecking=accept-new"
git clone git@git.example.com:platform/gitops-config.git cfg && cd cfg
yq -i ".image.tag = \\"$TAG\\"" apps/orders-api/dev/values.yaml
git commit -am "orders-api: dev -> $TAG" && git push
'''
}
}
}
See "GitOps Principles & Practice", lesson 03, for promotion from dev to prod.
Try it: commit to registry
- Take a small app with a Dockerfile and unit tests; publish JUnit results in
post { always }. - Build and push with rootless BuildKit or Buildah on a Kubernetes agent to a test registry, tagged with the commit SHA.
- Add the Trivy stage and make it fail on a deliberately old base image; fix the base image.
- Add the GitOps promotion stage against a test config repository and check the commit it makes.
Recap
- Publish test reports; build → scan → push → record digest.
- Rootless builds (BuildKit, Buildah); no Docker socket, no privileged DinD.
- Tag by commit SHA and version; deploy by digest; never
latest. - Prefer handing over via the GitOps config repo instead of pushing to clusters.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.