Using a Jenkinsfile to configure the Jenkins build job for source code is great. Jenkins has a very nice Docker Pipeline plugin that makes it possible to execute docker commands nicely during the build.
Note: Don’t forget to read on this page the update of 16 august 2018.
However, a lot of the examples at https://jenkins.io/doc/book/pipeline/docker/ keep it very simple. They start and stop in one pipeline stage, with methods like docker.inside or docker.withRun. For example, building a container, running it, executing commands in it and destroy it, all within one stage. For several use cases this is fine, but for building an application Docker container, it is much nicer to implement multiple stages.
In the more advanced example on this page, I also used a pipelineContext global variable which is of type LinkedHashMap. The Jenkinsfile programming language is Groovy. In the Groovy this comes close to the equivalent of the JavaScript object. This variable makes it possible to share data or objects between stages. Let’s take a look at the “Run” stage.
stage('Run') { steps { echo "Run docker image" script { pipelineContext.dockerContainer = pipelineContext.dockerImage.run() } } }
After running a Docker container based on an image, a container instance is returned. In order to be able to stop and remove the container if the build fails or is finished, you will need this container object.
post { always { echo "Stop Docker image" script { if (pipelineContext && pipelineContext.dockerContainer) { pipelineContext.dockerContainer.stop() } } } }
Below is a complete example of a Declarative Pipeline that spreads several Docker commands across multiple stages.
// Initialize a LinkedHashMap / object to share between stages def pipelineContext = [:] pipeline { agent any environment { DOCKER_IMAGE_TAG = "my-app:build-${env.BUILD_ID}" } stages { stage('Configure') { steps { echo 'Create parameters file' } } stage('Build') { steps { echo "Build docker image" script { dockerImage = docker.build("${env.DOCKER_IMAGE_TAG}", '-f ./Dockerfile .') pipelineContext.dockerImage = dockerImage } } } stage('Run') { steps { echo "Run docker image" script { pipelineContext.dockerContainer = pipelineContext.dockerImage.run() } } } stage('Test') { steps { echo "Testing the app" } } stage('Push') { steps { echo "Pushing the Docker image to the registry" } } stage('Deploy') { steps { echo "Deploying the Docker image" } } stage('Verify') { parallel { stage('Verify home') { agent any steps { echo "HTTP request to verify home" } } stage('Verify health check') { agent any steps { echo "HTTP request to verify application health check" } } stage('Verify regression tests') { agent any steps { echo "Running regression test suite" } } } } } post { always { echo "Stop Docker image" script { if (pipelineContext && pipelineContext.dockerContainer) { pipelineContext.dockerContainer.stop() } } } } }
I’m not sure why I experienced problems by the time of writing this post. The use of a global pipelineContext variable is not required (anymore?). Variables can be referenced across stages.
pipeline { agent any stages { stage('Build') { steps { script { docker_image = docker.build("${env.DOCKER_IMAGE_TAG}", '-f ./Dockerfile .') } } } stage('Test') { parallel { stage('Unit tests') { agent any steps { script { docker_image.inside("--entrypoint='/start.sh'") { sh 'cd /var/www/app && vendor/bin/phpunit --testsuite=Unittest' } } } } stage('Health check') { agent any steps { script { docker_image.inside("--entrypoint='/start.sh'") { timeout(time: 1, unit: 'MINUTES') { retry(5) { sleep 5 sh "curl -sS http://localhost/info | grep 'My API'" } } } } } } } } } }