Jenkins — Level by Level›03 · Build, test and publish images

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.

Practitioner
Key wordsJenkinscontainer buildBuildKitbuildctlBuildahrootlessDocker socketimage tagdigestTrivyJUnitarchiveArtifactsGitOps hand-off

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

  1. Take a small app with a Dockerfile and unit tests; publish JUnit results in post { always }.
  2. Build and push with rootless BuildKit or Buildah on a Kubernetes agent to a test registry, tagged with the commit SHA.
  3. Add the Trivy stage and make it fail on a deliberately old base image; fix the base image.
  4. 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.