How-To Guides
Step-by-step guides for various technologies
1. Available Guides
- How to Write Dockerfiles - Best practices for efficient Dockerfiles
- How to Write Docker Compose Files - Organizing multi-container applications
- How to Write Jenkinsfiles - Complete Jenkins pipeline guide (10 articles)
- Saml2Aws Setup - AWS access with SAML authentication
2. Getting Started
Select a guide from the sidebar to begin.
Articles in this section
| Title | Description | Updated |
|---|
| Saml2Aws Setup | Guide to setting up and using Saml2Aws for AWS access | 2026-05-15 22:23:29 +0200 +0200 |
| How to Write Dockerfiles | Best practices for writing efficient and secure Dockerfiles | 2026-05-15 22:23:28 +0200 +0200 |
| How to Write Docker Compose Files | Guide to writing and organizing Docker Compose files | 2026-05-15 22:23:28 +0200 +0200 |
| Debug Hugo | Debug Hugo: Quick guide to locate Hugo templates, use templateMetrics, override priorities, and fix common Docsy/Hugo template issues for faster debugging. | 2026-05-15 22:23:28 +0200 +0200 |
1 - How to Write Jenkinsfiles
Comprehensive guide to writing Jenkins pipelines and Jenkinsfiles
1. What You’ll Learn
- How Jenkins works and its architecture
- Declarative and scripted pipeline syntax
- Creating and using Jenkins shared libraries
- Jenkins best practices and configuration
- Real-world Jenkinsfile examples with detailed annotations
- Common recipes and troubleshooting tips
1.1 - How Jenkins Works
Understanding Jenkins architecture and concepts
Source: https://www.jenkins.io/doc/book/managing/nodes/
Source glossary: https://www.jenkins.io/doc/book/glossary/
1. Jenkins Master Slave Architecture

The Jenkins controller is the master node which is able to launch jobs on different nodes (machines)
directed by an Agent. The Agent can the use one or several executors to execute the job(s) depending on
configuration.
Jenkins is using Master/Slave architecture with the following components:
1.1. Jenkins controller/Jenkins master node
The central, coordinating process which stores configuration, loads plugins, and renders the various user interfaces
for Jenkins.
The Jenkins controller is the Jenkins service itself and is where Jenkins is installed. It is a webserver that also acts
as a “brain” for deciding how, when and where to run tasks. Management tasks (configuration, authorization, and
authentication) are executed on the controller, which serves HTTP requests. Files written when a Pipeline executes are
written to the filesystem on the controller unless they are off-loaded to an artifact repository such as Nexus or
Artifactory.
1.2. Nodes
A machine which is part of the Jenkins environment and capable of executing
Pipelines or
jobs. Both the
Controller and
Agents are considered to be Nodes.
Nodes are the “machines” on which build agents run. Jenkins monitors each attached node for disk space, free temp
space, free swap, clock time/sync and response time. A node is taken offline if any of these values go outside the
configured threshold.
The Jenkins controller itself runs on a special built-in node. It is possible to run agents and executors on this
built-in node although this can degrade performance, reduce scalability of the Jenkins instance, and create serious
security problems and is strongly discouraged, especially for production environments.
1.3. Agents
An agent is typically a machine, or container, which connects to a Jenkins controller and executes tasks when directed
by the controller.
Agents manage the task execution on behalf of the Jenkins controller by using executors. An agent is actually a small
(170KB single jar) Java client process that connects to a Jenkins controller and is assumed to be unreliable. An agent
can use any operating system that supports Java. Tools required for builds and tests are installed on the node where the
agent runs; they can be installed directly or in a container (Docker or Kubernetes). Each agent is effectively a process
with its own PID (Process Identifier) on the host machine.
In practice, nodes and agents are essentially the same but it is good to remember that they are conceptually distinct.
1.4. Executors
A slot for execution of work defined by a Pipeline or
job on a Node. A
Node may have zero or more Executors configured which corresponds to how many concurrent Jobs or Pipelines are able to
execute on that Node.
An executor is a slot for execution of tasks; effectively, it is a thread in the agent. The number of executors on a
node defines the number of concurrent tasks that can be executed on that node at one time. In other words, this
determines the number of concurrent Pipeline stages that can execute on that node at one time.
The proper number of executors per build node must be determined based on the resources available on the node and the
resources required for the workload. When determining how many executors to run on a node, consider CPU and memory
requirements as well as the amount of I/O and network activity:
- One executor per node is the safest configuration.
- One executor per CPU core may work well if the tasks being run are small.
- Monitor I/O performance, CPU load, memory usage, and I/O throughput carefully when running multiple executors on a
node.
1.5. Jobs
A user-configured description of work which Jenkins should perform, such as building a piece of software, etc.
2. Jenkins dynamic node
Jenkins has static slave nodes and can trigger the generation of dynamic slave nodes

1.2 - Jenkins Pipelines
Declarative and scripted pipeline syntax
1. What is a pipeline ?
https://www.jenkins.io/doc/book/pipeline/
Jenkins Pipeline (or simply “Pipeline” with a capital “P”) is a suite of plugins which supports implementing and
integrating continuous delivery pipelines into Jenkins.
A continuous delivery (CD) pipeline is an automated expression of your process for getting software from version
control right through to your users and customers. Every change to your software (committed in source control) goes
through a complex process on its way to being released. This process involves building the software in a reliable and
repeatable manner, as well as progressing the built software (called a “build”) through multiple stages of testing and
deployment.
Pipeline provides an extensible set of tools for modeling simple-to-complex delivery pipelines “as code” via the
Pipeline domain-specific language (DSL) syntax.
View footnote 1
The definition of a Jenkins Pipeline is written into a text file (called a
Jenkinsfile) which in turn can be committed to a project’s
source control repository.
View footnote 2 This is the foundation of
“Pipeline-as-code”; treating the CD pipeline a part of the application to be versioned and reviewed like any other
code.
2. Pipeline creation via UI
it’s not recommended but it’s possible to create a pipeline via the UI.
There are several drawbacks:
- no code revision
- difficult to read, understand
3. Groovy
Scripted and declarative pipelines are using groovy language.
Checkout https://www.guru99.com/groovy-tutorial.html to have a quick
overview of this derived language check Wikipedia
4. Difference between scripted pipeline (freestyle) and declarative pipeline syntax
What are the main differences ? Here are some of the most important things you should know:
- Basically, declarative and scripted pipelines differ in terms of the programmatic approach. One uses a declarative
programming model and the second uses an imperative programming mode.
- Declarative pipelines break down stages into multiple steps, while in scripted pipelines there is no need for this.
Example below
Declarative and Scripted Pipelines are constructed fundamentally differently. Declarative Pipeline is a more recent
feature of Jenkins Pipeline which:
- provides richer syntactical features over Scripted Pipeline syntax, and
- is designed to make writing and reading Pipeline code easier.
- By default automatically checkout stage
Many of the individual syntactical components (or “steps”) written into a Jenkinsfile, however, are common to both
Declarative and Scripted Pipeline. Read more about how these two types of syntax differ in
Pipeline concepts and
Pipeline syntax overview.
5. Declarative pipeline example
Pipeline syntax documentation
pipeline {
agent {
// executed on an executor with the label 'some-label'
// or 'docker', the label normally specifies:
// - the size of the machine to use
// (eg.: Docker-C5XLarge used for build that needs a powerful machine)
// - the features you want in your machine
// (eg.: docker-base-ubuntu an image with docker command available)
label "some-label"
}
stages {
stage("foo") {
steps {
// variable assignment and Complex global
// variables (with properties or methods)
// can only be done in a script block
script {
foo = docker.image('ubuntu')
env.bar = "${foo.imageName()}"
echo "foo: ${foo.imageName()}"
}
}
}
stage("bar") {
steps{
echo "bar: ${env.bar}"
echo "foo: ${foo.imageName()}"
}
}
}
}
6. Scripted pipeline example
Scripted pipelines permit a developer to inject code, while the declarative Jenkins pipeline doesn’t. should be
avoided actually, try to use jenkins library instead
node {
git url: 'https://github.com/jfrogdev/project-examples.git'
// Get Artifactory server instance, defined in the Artifactory Plugin
// administration page.
def server = Artifactory.server "SERVER_ID"
// Read the upload spec and upload files to Artifactory.
def downloadSpec =
'''{
"files": [
{
"pattern": "libs-snapshot-local/*.zip",
"target": "dependencies/",
"props": "p1=v1;p2=v2"
}
]
}'''
def buildInfo1 = server.download spec: downloadSpec
// Read the upload spec which was downloaded from github.
def uploadSpec =
'''{
"files": [
{
"pattern": "resources/Kermit.*",
"target": "libs-snapshot-local",
"props": "p1=v1;p2=v2"
},
{
"pattern": "resources/Frogger.*",
"target": "libs-snapshot-local"
}
]
}'''
// Upload to Artifactory.
def buildInfo2 = server.upload spec: uploadSpec
// Merge the upload and download build-info objects.
buildInfo1.append buildInfo2
// Publish the build to Artifactory
server.publishBuildInfo buildInfo1
}
7. Why Pipeline?
Jenkins is, fundamentally, an automation engine which supports a number of automation patterns. Pipeline adds a powerful
set of automation tools onto Jenkins, supporting use cases that span from simple continuous integration to comprehensive
CD pipelines. By modeling a series of related tasks, users can take advantage of the many features of Pipeline:
- Code: Pipelines are implemented in code and typically checked into source control, giving teams the ability to
edit, review, and iterate upon their delivery pipeline.
- Durable: Pipelines can survive both planned and unplanned restarts of the Jenkins controller.
- Pausable: Pipelines can optionally stop and wait for human input or approval before continuing the Pipeline run.
- Versatile: Pipelines support complex real-world CD requirements, including the ability to fork/join, loop, and
perform work in parallel.
- Extensible: The Pipeline plugin supports custom extensions to its DSL
see jenkins doc and multiple options for integration with
other plugins.
While Jenkins has always allowed rudimentary forms of chaining Freestyle Jobs together to perform sequential tasks,
see jenkins doc Pipeline makes this concept a first-class
citizen in Jenkins.
More information on Official jenkins documentation - Pipeline
1.3 - Jenkins Library
Creating and using Jenkins shared libraries
1. What is a jenkins shared library ?
As Pipeline is adopted for more and more projects in an organization, common patterns are likely to emerge. Oftentimes
it is useful to share parts of Pipelines between various projects to reduce redundancies and keep code “DRY”
for more information check pipeline shared libraries
2. Loading libraries dynamically
As of version 2.7 of the Pipeline: Shared Groovy Libraries plugin, there is a new option for loading (non-implicit)
libraries in a script: a library step that loads a library dynamically, at any time during the build.
If you are only interested in using global variables/functions (from the vars/ directory), the syntax is quite simple:
library 'my-shared-library'
Thereafter, any global variables from that library will be accessible to the script.
3. jenkins library directory structure
The directory structure of a Shared Library repository is as follows:
(root)
+- src # Groovy source files
| +- org
| +- foo
| +- Bar.groovy # for org.foo.Bar class
|
+- vars # The vars directory hosts script
# files that are exposed as a variable in Pipelines
| +- foo.groovy # for global 'foo' variable
| +- foo.txt # help for 'foo' variable
|
+- resources # resource files (external libraries only)
| +- org
| +- foo
| +- bar.json # static helper data for org.foo.Bar
4. Jenkins library
remember that jenkins library code is executed on master node
if you want to execute code on the node, you need to use jenkinsExecutor
usage of jenkins executor
String credentialsId = 'babee6c1-14fe-4d90-9da0-ffa7068c69af'
def lib = library(
identifier: '[email protected]',
retriever: modernSCM([
$class: 'GitSCMSource',
remote: '[email protected]:fchastanet/jenkins-library.git',
credentialsId: credentialsId
])
)
// this is the jenkinsExecutor instance
def docker = lib.fchastanet.Docker.new(this)
Then in the library, it is used like this:
def status = this.jenkinsExecutor.sh(
script: "docker pull ${cacheTag}", returnStatus: true
)
5. Jenkins library structure
I remarked that a lot of code was duplicated between all my Jenkinsfiles so I created this library
https://github.com/fchastanet/jenkins-library
(root)
+- doc # markdown files automatically generated
# from groovy files by generateDoc.sh
+- src # Groovy source files
| +- fchastanet
| +- Cloudflare.groovy # zonePurge
| +- Docker.groovy # getTagCompatibleFromBranch
# pullBuildPushImage, ...
| +- Git.groovy # getRepoURL, getCommitSha,
# getLastPusherEmail,
# updateConditionalGithubCommitStatus
| +- Kubernetes.groovy # deployHelmChart, ...
| +- Lint.groovy # dockerLint,
# transform lighthouse report
# to Warnings NG issues format
| +- Mail.groovy # sendTeamsNotification,
# sendConditionalEmail, ...
| +- Utils.groovy # deepMerge, isCollectionOrArray,
# deleteDirAsRoot,
# initAws (could be moved to Aws class)
+- vars # The vars directory hosts script files that
# are exposed as a variable in Pipelines
| +- dockerPullBuildPush.groovy #
| +- whenOrSkip.groovy #
6. external resource usage
If you need you check out how I used this repository https://github.com/fchastanet/jenkins-library-resources in
jenkins_library (Linter) that hosts some resources to parse result files.
1.4 - Jenkins Best Practices
Best practices and patterns for Jenkins and Jenkinsfiles
1. Pipeline best practices
Official Jenkins pipeline best practices
Summary:
- Make sure to use Groovy code in Pipelines as glue
- Externalize shell scripts from Jenkins Pipeline
- for better jenkinsfile readability
- in order to test the scripts isolated from jenkins
- Avoid complex Groovy code in Pipelines
- Groovy code always executes on controller which means using controller resources(memory and CPU)
- it is not the case for shell scripts
- eg1: prefer using jq inside shell script instead of groovy JsonSlurper
- eg2: prefer calling curl instead of groovy http request
- Reducing repetition of similar Pipeline steps (eg: one sh step instead of severals)
- group similar steps together to avoid step creation/destruction overhead
- Avoiding calls to Jenkins.getInstance
2. Shared library best practices
Official Jenkins shared libraries best practices
Summary:
- Do not override built-in Pipeline steps
- Avoiding large global variable declaration files
- Avoiding very large shared libraries
And:
- import jenkins library using a tag
- like in docker build, npm package with package-lock.json or python pip lock, it’s advised to target a given version
of the library
- because some changes could break
- The missing part: we miss on this library unit tests
- but each pipeline is a kind of integration test
- Because a pipeline can be
resumed, your
library’s classes should implement Serializable class and the following attribute has to be provided:
private static final long serialVersionUID = 1L
1.5 - Annotated Jenkinsfiles - Part 1
Detailed Jenkinsfile examples with annotations
Pipeline example
1. Simple one
This build is used to generate docker images used to build production code and launch phpunit tests. This pipeline is
parameterized in the Jenkins UI directly with the parameters:
- branch (git branch to use)
- environment(select with 3 options: build, phpunit or all)
- it would have been better to use simply 2 checkboxes phpunit/build
- project_branch
Here the source code with inline comments:
Annotated jenkinsfile Expand source
// This method allows to convert the branch name to a docker image tag.
// This method is generally used by most of my jenkins pipelines, it's why it has been added to https://github.com/fchastanet/jenkins-library/blob/master/src/fchastanet/Docker.groovy#L31
def getTagCompatibleFromBranch(String branchName) {
def String tag = branchName.toLowerCase()
tag = tag.replaceAll("^origin/", "")
return tag.replaceAll('/', '_')
}
// we declare here some variables that will be used in next stages
def String deploymentBranchTagCompatible = ''
pipeline {
agent {
node {
// the pipeline is executed on a machine with docker daemon
// available
label 'docker-ubuntu'
}
}
stages {
stage ('checkout') {
steps {
// this command is actually not necessary because checkout is
// done automatically when using declarative pipeline
sh 'echo "pulling ... ${GIT_BRANCH#origin/}"'
checkout scm
// this particular build needs to access to some private github
// repositories, so here we are copying the ssh key
// it would be better to use new way of injecting ssh key
// inside docker using sshagent
// check https://stackoverflow.com/a/66897280
withCredentials([
sshUserPrivateKey(
credentialsId: '855aad9f-1b1b-494c-aa7f-4de881c7f659',
keyFileVariable: 'sshKeyFile'
)
]) {
// best practice similar steps should be merged into one
sh 'rm -f ./phpunit/id_rsa'
sh 'rm -f ./build/id_rsa'
// here we are escaping '$' so the variable will be
// interpolated on the jenkins slave and not the jenkins
// master node instead of escaping, we could have used
// single quotes
sh "cp \$sshKeyFile ./phpunit/id_rsa"
sh "cp \$sshKeyFile ./build/id_rsa"
}
script {
// as actually scm is already done before executing the
// first step, this call could have been done during
// declaration of this variable
deploymentBranchTagCompatible = getTagCompatibleFromBranch(GIT_BRANCH)
}
}
}
stage("build Build env") {
when {
// the build can be launched with the parameter environment
// defined in the configuration of the jenkins job, these
// parameters could have been defined directly in the pipeline
// see https://www.jenkins.io/doc/book/pipeline/syntax/#parameters
expression { return params.environment != "phpunit"}
}
steps {
// here we could have launched all this commands in the same sh
// directive
sh "docker build --build-arg BRANCH=${params.project_branch} -t build build"
// use a constant for dockerRegistryId.dkr.ecr.eu-west-1.amazonaws.com
sh "docker tag build dockerRegistryId.dkr.ecr.eu-west-1.amazonaws.com/build:${deploymentBranchTagCompatible}"
sh "docker push dockerRegistryId.dkr.ecr.eu-west-1.amazonaws.com/build:${deploymentBranchTagCompatible}"
}
}
stage("build PHPUnit env") {
when {
// it would have been cleaner to use
// expression { return params.environment = "phpunit"}
expression { return params.environment != "build"}
}
steps {
sh "docker build --build-arg BRANCH=${params.project_branch} -t phpunit phpunit"
sh "docker tag phpunit dockerRegistryId.dkr.ecr.eu-west-1.amazonaws.com/phpunit:${deploymentBranchTagCompatible}"
sh "docker push dockerRegistryId.dkr.ecr.eu-west-1.amazonaws.com/phpunit:${deploymentBranchTagCompatible}"
}
}
}
}
without seeing the Dockerfile files, we can advise :
- to build these images in the same pipeline where build and phpunit are run
- the images are built at the same time so we are sure that we are using the right version
- apparently the docker build depend on the branch of the project, this should be avoided
- ssh key is used in docker image, that could lead to a security issue as ssh key is still in the history of images
layers even if it has been removed in subsequent layers, check https://stackoverflow.com/a/66897280 for information
on how to use ssh-agent instead
- we could use a single Dockerfile with 2 stages:
- one stage to generate production image
- one stage that inherits production stage, used to execute phpunit
- it has the following advantages :
- reduce the total image size because of the reuse different docker image layers
- only one Dockerfile to maintain
2. More advanced and annotated Jenkinsfiles
1.6 - Annotated Jenkinsfiles - Part 2
More annotated Jenkinsfile examples
1. Introduction
This example is missing the use of parameters, jenkins library in order to reuse common code
This example uses :
- post conditions
https://www.jenkins.io/doc/book/pipeline/syntax/#post
- github plugin to set commit status indicating the result of the build
- usage of several jenkins plugins, you can check here to get the full list installed on your server and even
generate code snippets by adding pipeline-syntax/ to your jenkins server url
But it misses:
check Pipeline syntax documentation
2. Annotated Jenkinsfile
// Define variables for QA environment
def String registry_id = 'awsAccountId'
def String registry_url = registry_id + '.dkr.ecr.us-east-1.amazonaws.com'
def String image_name = 'project'
def String image_fqdn_master = registry_url + '/' + image_name + ':master'
def String image_fqdn_current_branch = image_fqdn_master
// this method is used by several of my pipelines and has been added
// to jenkins_library <https://github.com/fchastanet/jenkins-library/blob/master/src/fchastanet/Git.groovy#L156>
void publishStatusToGithub(String status) {
step([
$class: "GitHubCommitStatusSetter",
reposSource: [$class: "ManuallyEnteredRepositorySource", url: "https://github.com/fchastanet/project"],
errorHandlers: [[$class: 'ShallowAnyErrorHandler']],
statusResultSource: [
$class: 'ConditionalStatusResultSource',
results: [
[$class: 'AnyBuildResult', state: status]
]
]
]);
}
pipeline {
agent {
node {
// bad practice: try to indicate in your node labels, which feature it
// includes for example, here we need docker, label could have been
// 'eks-nonprod-docker'
label 'eks-nonprod'
}
}
stages {
stage ('Checkout') {
steps {
// checkout is not necessary as it is automatically done
checkout scm
script {
// 'wrap' allows to inject some useful variables like BUILD_USER,
// BUILD_USER_FIRST_NAME
// see https://www.jenkins.io/doc/pipeline/steps/build-user-vars-plugin/
wrap([$class: 'BuildUser']) {
def String displayName = "#${currentBuild.number}_${BRANCH}_${BUILD_USER}_${DEPLOYMENT}"
// params could have been defined inside the pipeline directly
// instead of defining them in jenkins build configuration
if (params.DEPLOYMENT == 'staging') {
displayName = "${displayName}_${INSTANCE}"
}
// next line allows to change the build name, check addHtmlBadge
// plugin function for more advanced usage of this feature, you
// check this jenkinsfile 05-02-Annotated-Jenkinsfiles.md
currentBuild.displayName = displayName
}
}
}
}
stage ('Run tests') {
steps {
// all these sh directives could have been merged into one
// it is best to use a separated sh file that could take some parameters
// as it is simpler to read and to eventually test separately
sh 'docker build -t project-test "$PWD"/docker/test'
sh 'cp "$PWD"/app/config/parameters.yml.dist "$PWD"/app/config/parameters.yml'
// for better readability and if separated script is not possible, use
// continuation line for better readability
sh 'docker run -i --rm -v "$PWD":/var/www/html/ -w /var/www/html/ project-test /bin/bash -c "composer install -a && ./bin/phpunit -c /var/www/html/app/phpunit.xml --coverage-html /var/www/html/var/logs/coverage/ --log-junit /var/www/html/var/logs/phpunit.xml --coverage-clover /var/www/html/var/logs/clover_coverage.xml"'
}
// Run the steps in the post section regardless of the completion status
// of the Pipeline’s or stage’s run.
// see https://www.jenkins.io/doc/book/pipeline/syntax/#post
post {
always {
// report unit test reports (unit test should generate result using
// using junit format)
junit 'var/logs/phpunit.xml'
// generate coverage page from test results
step([
$class: 'CloverPublisher',
cloverReportDir: 'var/logs/',
cloverReportFileName: 'clover_coverage.xml'
])
// publish html page with the result of the coverage
publishHTML(
target: [
allowMissing: false,
alwaysLinkToLastBuild: false,
keepAll: true,
reportDir: 'var/logs/coverage/',
reportFiles: 'index.html',
reportName: "Coverage Report"
]
)
}
}
}
// this stage will be executed only if previous stage is successful
stage('Build image') {
when {
// this stage is executed only if these conditions returns true
expression {
return
params.DEPLOYMENT == "staging"
|| (
params.DEPLOYMENT == "prod"
&& env.GIT_BRANCH == 'origin/master'
)
}
}
steps {
script {
// this code is used in most of the pipeline and has been centralized
// in https://github.com/fchastanet/jenkins-library/blob/master/src/fchastanet/Git.groovy#L39
env.IMAGE_TAG = env.GIT_COMMIT.substring(0, 7)
// Update variable for production environment
if ( params.DEPLOYMENT == 'prod' ) {
registry_id = 'awsDockerRegistryId'
registry_url = registry_id + '.dkr.ecr.eu-central-1.amazonaws.com'
image_fqdn_master = registry_url + '/' + image_name + ':master'
}
image_fqdn_current_branch = registry_url + '/' + image_name + ':' + env.IMAGE_TAG
}
// As jenkins slave machine can be constructed on demand,
// it doesn't always contains all docker image cache
// here to avoid building docker image from scratch, we are trying to
// pull an existing version of the docker image on docker registry
// and then build using this image as cache, so all layers not updated
// in Dockerfile will not be built again (gain of time)
// It is again a recurrent usage in most of the pipelines
// so the next 8 lines could be replaced by the call to this method
// Docker
// pullBuildPushImage https://github.com/fchastanet/jenkins-library/blob/master/src/fchastanet/Docker.groovy#L46
// Pull the master from repository (|| true avoids errors if the image
// hasn't been pushed before)
sh "docker pull ${image_fqdn_master} || true"
// Build the image using pulled image as cache
// instead of using concatenation, it is more readable to use variable interpolation
// Eg: "docker build --cache-from ${image_fqdn_master} -t ..."
sh 'docker build \
--cache-from ' + image_fqdn_master + ' \
-t ' + image_name + ' \
-f "$PWD/docker/prod/Dockerfile" \
.'
}
}
stage('Deploy image (Staging)') {
when {
expression { return params.DEPLOYMENT == "staging" }
}
steps {
script {
// Actually we should always push the image in order to be able to
// feed the docker cache for next builds
// Again the method Docker pullBuildPushImage https://github.com/fchastanet/jenkins-library/blob/master/src/fchastanet/Docker.groovy#L46
// solves this issue and could be used instead of the next 6 lines
// and "Push image (Prod)" stage
// If building master, we should push the image with the tag master
// to benefit from docker cache
if ( env.GIT_BRANCH == 'origin/master' ) {
sh label:"Tag the image as master",
script:"docker tag ${image_name} ${image_fqdn_master}"
sh label:"Push the image as master",
script:"docker push ${image_fqdn_master}"
}
}
sh label:"Tag the image", script:"docker tag ${image_name} ${image_fqdn_current_branch}"
sh label:"Push the image", script:"docker push ${image_fqdn_current_branch}"
// use variable interpolation instead of concatenation
sh label:"Deploy on cluster", script:" \
helm3 upgrade project-" + params.INSTANCE + " -i \
--namespace project-" + params.INSTANCE + " \
--create-namespace \
--cleanup-on-fail \
--atomic \
-f helm/values_files/values-" + params.INSTANCE + ".yaml \
--set deployment.php_container.image.pullPolicy=Always \
--set image.tag=" + env.IMAGE_TAG + " \
./helm"
}
}
stage('Push image (Prod)') {
when {
expression { return params.DEPLOYMENT == "prod" && env.GIT_BRANCH == 'origin/master'}
}
// The method Docker pullBuildPushImage https://github.com/fchastanet/jenkins-library/blob/master/src/fchastanet/Docker.groovy#L46
// provides a generic way of managing the pull, build, push of the docker
// images, by managing also a common way of tagging docker images
steps {
sh label:"Tag the image as master", script:"docker tag ${image_name} ${image_fqdn_current_branch}"
sh label:"Push the image as master", script:"docker push ${image_fqdn_current_branch}"
}
}
}
post {
always {
// mark github commit as built
publishStatusToGithub("${currentBuild.currentResult}")
}
}
}
This directive is really difficult to read and eventually debug it
sh 'docker run -i --rm -v "$PWD":/var/www/html/ -w /var/www/html/ project-test /bin/bash -c "composer install -a && ./bin/phpunit -c /var/www/html/app/phpunit.xml --coverage-html /var/www/html/var/logs/coverage/ --log-junit /var/www/html/var/logs/phpunit.xml --coverage-clover /var/www/html/var/logs/clover_coverage.xml"'
Another way to write previous directive is to:
- use continuation line
- avoid ‘&&’ as it can mask errors, use ‘;’ instead
- use ‘set -o errexit’ to fail on first error
- use ‘set -o pipefail’ to fail if eventual piped command is failing
- ‘set -x’ allows to trace every command executed for better debugging
Here a possible refactoring:
sh ''''
docker run -i --rm \
-v "$PWD":/var/www/html/ \
-w /var/www/html/ \
project-test \
/bin/bash -c "\
set -x ;\
set -o errexit ;\
set -o pipefail ;\
composer install -a ;\
./bin/phpunit \
-c /var/www/html/app/phpunit.xml \
--coverage-html /var/www/html/var/logs/coverage/ \
--log-junit /var/www/html/var/logs/phpunit.xml \
--coverage-clover /var/www/html/var/logs/clover_coverage.xml
"
'''
Note however it is best to use a separated sh file(s) that could take some parameters as it is simpler to read and to
eventually test separately. Here a refactoring using a separated sh file:
runTests.sh
#!/bin/bash
set -x -o errexit -o pipefail
composer install -a
./bin/phpunit \
-c /var/www/html/app/phpunit.xml \
--coverage-html /var/www/html/var/logs/coverage/ \
--log-junit /var/www/html/var/logs/phpunit.xml \
--coverage-clover /var/www/html/var/logs/clover_coverage.xml
jenkinsRunTests.sh
#!/bin/bash
set -x -o errexit -o pipefail
docker build -t project-test "${PWD}/docker/test"
docker run -i --rm \
-v "${PWD}:/var/www/html/" \
-w /var/www/html/ \
project-test \
runTests.sh
Then the sh directive becomes simply
1.7 - Annotated Jenkinsfiles - Part 3
Additional Jenkinsfile pattern examples
1. Introduction
This build will:
- pull/build/push docker image used to generate project files
- lint
- run Unit tests with coverage
- build the SPA
- run accessibility tests
- build story book and deploy it
- deploy spa on s3 bucket and refresh cloudflare cache
It allows to build for production and qa stages allowing different instances. Every build contains:
- a summary of the build
- git branch
- git revision
- target environment
- all the available Urls:
2. Annotated Jenkinsfile
// anonymized parameters
String credentialsId = 'jenkinsCredentialId'
def lib = library(
identifier: '[email protected]',
retriever: modernSCM([
$class: 'GitSCMSource',
remote: '[email protected]:fchastanet/jenkins-library.git',
credentialsId: credentialsId
])
)
def docker = lib.fchastanet.Docker.new(this)
def git = lib.fchastanet.Git.new(this)
def mail = lib.fchastanet.Mail.new(this)
def utils = lib.fchastanet.Utils.new(this)
def cloudflare = lib.fchastanet.Cloudflare.new(this)
// anonymized parameters
String CLOUDFLARE_ZONE_ID = 'cloudflareZoneId'
String CLOUDFLARE_ZONE_ID_PROD = 'cloudflareZoneIdProd'
String REGISTRY_ID_QA = 'dockerRegistryId'
String REACT_APP_PENDO_API_KEY = 'pendoApiKey'
String REGISTRY_QA = REGISTRY_ID_QA + '.dkr.ecr.us-east-1.amazonaws.com'
String IMAGE_NAME_SPA = 'project-ui'
String STAGING_API_URL = 'https://api.host'
String INSTANCE_URL = "https://${params.instanceName}.host"
String REACT_APP_API_BASE_URL_PROD = 'https://ui.host'
String REACT_APP_PENDO_SOURCE_DOMAIN = 'https://cdn.eu.pendo.io'
String buildBucketPrefix
String S3_PUBLIC_URL = 'qa-spa.s3.amazonaws.com/project'
String S3_PROD_PUBLIC_URL = 'spa.s3.amazonaws.com/project'
List<String> instanceChoices = (1..20).collect { 'project' + it }
Map buildInfo = [
apiUrl: '',
storyBookAvailable: false,
storyBookUrl: '',
storyBookDocsUrl: '',
spaAvailable: false,
spaUrl: '',
instanceName: '',
]
// add information on summary page
def addBuildInfo(buildInfo) {
String deployInfo = ''
if (buildInfo.spaAvailable) {
String formatInstanceName = buildInfo.instanceName ?
" (${buildInfo.instanceName})" : '';
deployInfo += "<a href='${buildInfo.spaUrl}'>SPA${formatInstanceName}</a>"
}
if (buildInfo.storyBookAvailable) {
deployInfo += " / <a href='${buildInfo.storyBookUrl}'>Storybook</a>"
deployInfo += " / <a href='${buildInfo.storyBookDocsUrl}'>Storybook docs</a>"
}
String summaryHtml = """
<b>branch : </b>${GIT_BRANCH}<br/>
<b>revision : </b>${GIT_COMMIT}<br/>
<b>target env : </b>${params.targetEnv}<br/>
${deployInfo}
"""
removeHtmlBadges id: "htmlBadge${currentBuild.number}"
addHtmlBadge html: summaryHtml, id: "htmlBadge${currentBuild.number}"
}
pipeline {
agent {
node {
// this image has the features docker and lighthouse
label 'docker-base-ubuntu-lighthouse'
}
}
parameters {
gitParameter(
branchFilter: 'origin/(.*)',
defaultValue: 'main',
quickFilterEnabled: true,
sortMode: 'ASCENDING_SMART',
name: 'BRANCH',
type: 'PT_BRANCH'
)
choice(
name: 'targetEnv',
choices: ['none', 'testing', 'production'],
description: 'Where it should be deployed to? (Default: none - No deploy)'
)
booleanParam(
name: 'buildStorybook',
defaultValue: false,
description: 'Build Storybook (will only apply if selected targetEnv is testing)'
)
choice(
name: 'instanceName',
choices: instanceChoices,
description: 'Instance name to deploy the revision'
)
}
stages {
stage('Build SPA image') {
steps {
script {
// set build status to pending on github commit
step([$class: 'GitHubSetCommitStatusBuilder'])
wrap([$class: 'BuildUser']) {
currentBuild.displayName = "#${currentBuild.number}_${BRANCH}_${BUILD_USER}_${targetEnv}"
}
branchName = docker.getTagCompatibleFromBranch(env.GIT_BRANCH)
shortSha = git.getShortCommitSha(env.GIT_BRANCH)
if (params.targetEnv == 'production') {
buildBucketPrefix = GIT_COMMIT
buildInfo.apiUrl = REACT_APP_API_BASE_URL_PROD
s3BaseUrl = 's3://project-spa/project'
} else {
buildBucketPrefix = params.instanceName
buildInfo.instanceName = params.instanceName
buildInfo.spaUrl = "${INSTANCE_URL}/index.html"
buildInfo.apiUrl = STAGING_API_URL
s3BaseUrl = 's3://project-qa-spa/project'
buildInfo.storyBookUrl = "${INSTANCE_URL}/storybook/index.html"
buildInfo.storyBookDocsUrl = "${INSTANCE_URL}/storybook-docs/index.html"
}
addBuildInfo(buildInfo)
// Setup .env
sh """
set -x
echo "REACT_APP_API_BASE_URL = '${buildInfo.apiUrl}'" > ./.env
echo "REACT_APP_PENDO_SOURCE_DOMAIN = '${REACT_APP_PENDO_SOURCE_DOMAIN}'" >> ./.env
echo "REACT_APP_PENDO_API_KEY = '${REACT_APP_PENDO_API_KEY}'" >> ./.env
"""
withCredentials([
sshUserPrivateKey(
credentialsId: 'sshCredentialsId',
keyFileVariable: 'sshKeyFile')
]) {
docker.pullBuildPushImage(
buildDirectory: pwd(),
// use safer way to inject ssh key during docker build
buildArgs: "--ssh default=\$sshKeyFile --build-arg USER_ID=\$(id -u)",
registryImageUrl: "${REGISTRY_QA}/${IMAGE_NAME_SPA}",
tagPrefix: "${IMAGE_NAME_SPA}:",
localTagName: "latest",
tags: [
shortSha,
branchName
],
pullTags: ['main']
)
}
}
}
}
stage('Linting') {
steps {
sh """
docker run --rm \
-v ${env.WORKSPACE}:/app \
-v /app/node_modules \
${IMAGE_NAME_SPA} \
npm run lint
"""
}
}
stage('UT') {
steps {
script {
sh """docker run --rm \
-v ${env.WORKSPACE}:/app \
-v /app/node_modules \
${IMAGE_NAME_SPA} \
npm run test:coverage -- --ci
"""
junit 'output/junit.xml'
// https://plugins.jenkins.io/clover/
step([
$class: 'CloverPublisher',
cloverReportDir: 'output/coverage',
cloverReportFileName: 'clover.xml',
healthyTarget: [
methodCoverage: 70,
conditionalCoverage: 70,
statementCoverage: 70
],
// build will not fail but be set as unhealthy if coverage goes
// below 60%
unhealthyTarget: [
methodCoverage: 60,
conditionalCoverage: 60,
statementCoverage: 60
],
// build will fail if coverage goes below 50%
failingTarget: [
methodCoverage: 50,
conditionalCoverage: 50,
statementCoverage: 50
]
])
}
}
}
stage('Build SPA') {
steps {
script {
sh """
docker run --rm \
-v ${env.WORKSPACE}:/app \
-v /app/node_modules \
${IMAGE_NAME_SPA}
"""
}
}
}
stage('Accessibility tests') {
steps {
script {
// the pa11y-ci could have been made available in the node image
// to avoid installation each time, the build is launched
sh '''
sudo npm install -g serve pa11y-ci
serve -s build > /dev/null 2>&1 &
pa11y-ci --threshold 5 http://127.0.0.1:3000
'''
}
}
}
stage('Build Storybook') {
steps {
whenOrSkip(
params.targetEnv == 'testing'
&& params.buildStorybook == true
) {
script {
sh """
docker run --rm \
-v ${env.WORKSPACE}:/app \
-v /app/node_modules \
${IMAGE_NAME_SPA} \
sh -c 'npm run storybook:build -- --output-dir build/storybook \
&& npm run storybook:build-docs -- --output-dir build/storybook-docs'
"""
buildInfo.storyBookAvailable = true
}
}
}
}
stage('Artifacts to S3') {
steps {
whenOrSkip(params.targetEnv != 'none') {
script {
if (params.targetEnv == 'production') {
utils.initAws('arn:aws:iam::awsIamId:role/JenkinsSlave')
}
sh "aws s3 cp ${env.WORKSPACE}/build ${s3BaseUrl}/${buildBucketPrefix} --recursive --no-progress"
sh "aws s3 cp ${env.WORKSPACE}/build ${s3BaseUrl}/project1 --recursive --no-progress"
if (params.targetEnv == 'production') {
echo 'project SPA packages have been pushed to production bucket.'
echo '''You can refresh the production indexes with the CD
production pipeline.'''
cloudflare.zonePurge(CLOUDFLARE_ZONE_ID_PROD, [prefixes:[
"${S3_PROD_PUBLIC_URL}/project1/"
]])
} else {
cloudflare.zonePurge(CLOUDFLARE_ZONE_ID, [prefixes:[
"${S3_PUBLIC_URL}/${buildBucketPrefix}/"
]])
buildInfo.spaAvailable = true
publishChecks detailsURL: buildInfo.spaUrl,
name: 'projectSpaUrl',
title: 'project SPA url'
}
addBuildInfo(buildInfo)
}
}
}
}
}
post {
always {
script {
git.updateConditionalGithubCommitStatus()
mail.sendConditionalEmail()
}
}
}
}
1.8 - Annotated Jenkinsfiles - Part 4
Complex Jenkinsfile scenarios
1. introduction
The project aim is to create a browser extension available on chrome and firefox
This build allows to:
- lint the project using megalinter and phpstorm inspection
- build necessary docker images
- build firefox and chrome extensions
- deploy firefox extension on s3 bucket
- deploy chrome extension on google play store
2. Annotated Jenkinsfile
def credentialsId = 'jenkinsSshCredentialsId'
def lib = library(
identifier: 'jenkins_library',
retriever: modernSCM([
$class: 'GitSCMSource',
remote: '[email protected]:fchastanet/jenkins-library.git',
credentialsId: credentialsId
])
)
def docker = lib.fchastanet.Docker.new(this)
def git = lib.fchastanet.Git.new(this)
def mail = lib.fchastanet.Mail.new(this)
def String deploymentBranchTagCompatible = ''
def String gitShortSha = ''
def String REGISTRY_URL = 'dockerRegistryId.dkr.ecr.eu-west-1.amazonaws.com'
def String ECR_BROWSER_EXTENSION_BUILD = 'browser_extension_lint'
def String BUILD_TAG = 'build'
def String PHPSTORM_TAG = 'phpstorm-inspections'
def String REFERENCE_JOB_NAME = 'Browser_extension_deploy'
def String FIREFOX_S3_BUCKET = 'browser-extensions'
// it would have been easier to use checkboxes to avoid 'both'/'none'
// complexity
def DEPLOY_CHROME = (params.targetStore == 'both' || params.targetStore == 'chrome')
def DEPLOY_FIREFOX = (params.targetStore == 'both' || params.targetStore == 'firefox')
pipeline {
agent {
node {
label 'docker-base-ubuntu'
}
}
parameters {
gitParameter branchFilter: 'origin/(.*)',
defaultValue: 'master',
quickFilterEnabled: true,
sortMode: 'ASCENDING_SMART',
name: 'BRANCH',
type: 'PT_BRANCH'
choice (
name: 'targetStore',
choices: ['none', 'both', 'chrome', 'firefox'],
description: 'Where it should be deployed to? (Default: none, has effect only on master branch)'
)
}
environment {
GOOGLE_CREDS = credentials('GoogleApiChromeExtension')
GOOGLE_TOKEN = credentials('GoogleApiChromeExtensionCode')
GOOGLE_APP_ID = 'googleAppId'
// provided by https://addons.mozilla.org/en-US/developers/addon/api/key/
FIREFOX_CREDS = credentials('MozillaApiFirefoxExtension')
FIREFOX_APP_ID='{d4ce8a6f-675a-4f74-b2ea-7df130157ff4}'
}
stages {
stage("Init") {
steps {
script {
deploymentBranchTagCompatible = docker.getTagCompatibleFromBranch(env.GIT_BRANCH)
gitShortSha = git.getShortCommitSha(env.GIT_BRANCH)
echo "Branch ${env.GIT_BRANCH}"
echo "Docker tag = ${deploymentBranchTagCompatible}"
echo "git short sha = ${gitShortSha}"
}
sh 'echo StrictHostKeyChecking=no >> ~/.ssh/config'
}
}
stage("Lint") {
agent {
docker {
image 'megalinter/megalinter-javascript:v5'
args "-u root -v ${WORKSPACE}:/tmp/lint --entrypoint=''"
reuseNode true
}
}
steps {
sh 'npm install stylelint-config-rational-order'
sh '/entrypoint.sh'
}
}
stage("Build docker images") {
steps {
// whenOrSkip directive is defined in https://github.com/fchastanet/jenkins-library/blob/master/vars/whenOrSkip.groovy
whenOrSkip(currentBuild.currentResult == "SUCCESS") {
script {
docker.pullBuildPushImage(
buildDirectory: 'build',
registryImageUrl: "${REGISTRY_URL}/${ECR_BROWSER_EXTENSION_BUILD}",
tagPrefix: "${ECR_BROWSER_EXTENSION_BUILD}:",
tags: [
"${BUILD_TAG}_${gitShortSha}",
"${BUILD_TAG}_${deploymentBranchTagCompatible}",
],
pullTags: ["${BUILD_TAG}_master"]
)
}
}
}
}
stage("Build firefox/chrome extensions") {
steps {
whenOrSkip(currentBuild.currentResult == "SUCCESS") {
script {
sh """
docker run \
-v \$(pwd):/deploy \
--rm '${ECR_BROWSER_EXTENSION_BUILD}' \
/deploy/build/build-extensions.sh
"""
// multiple git statuses can be set on a given commit
// you can configure github to authorize pull request merge
// based on the presence of one or more github statuses
git.updateGithubCommitStatus("BUILD_OK")
}
}
}
}
stage("Deploy extensions") {
// deploy both extensions in parallel
parallel {
stage("Deploy chrome") {
steps {
whenOrSkip(currentBuild.currentResult == "SUCCESS" && DEPLOY_CHROME) {
// do not fail the entire build if this stage fail
// so firefox stage can be executed
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
script {
// best practice: complex sh files have been created outside
// of this jenkinsfile deploy-chrome-extension.sh
sh """
docker run \
-v \$(pwd):/deploy \
-e APP_CREDS_USR='${GOOGLE_CREDS_USR}' \
-e APP_CREDS_PSW='${GOOGLE_CREDS_PSW}' \
-e APP_TOKEN='${GOOGLE_APP_TOKEN}' \
-e APP_ID='${GOOGLE_APP_ID}' \
--rm '${ECR_BROWSER_EXTENSION_BUILD}' \
/deploy/build/deploy-chrome-extension.sh
"""
git.updateGithubCommitStatus("CHROME_DEPLOYED")
}
}
}
}
}
stage("Deploy firefox") {
steps {
whenOrSkip(currentBuild.currentResult == "SUCCESS" && DEPLOY_FIREFOX) {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
script {
// best practice: complex sh files have been created outside
// of this jenkinsfile deploy-firefox-extension.sh
sh """
docker run \
-v \$(pwd):/deploy \
-e FIREFOX_JWT_ISSUER='${FIREFOX_CREDS_USR}' \
-e FIREFOX_JWT_SECRET='${FIREFOX_CREDS_PSW}' \
-e FIREFOX_APP_ID='${FIREFOX_APP_ID}' \
--rm '${ECR_BROWSER_EXTENSION_BUILD}' \
/deploy/build/deploy-firefox-extension.sh
"""
sh """
set -x
set -o errexit
extensionVersion="\$(jq -r .version < package.json)"
extensionFilename="tools-\${extensionVersion}-an+fx.xpi"
echo "Upload new extension \${extensionFilename} to s3 bucket ${FIREFOX_S3_BUCKET}"
aws s3 cp "\$(pwd)/packages/\${extensionFilename}" "s3://${FIREFOX_S3_BUCKET}"
aws s3api put-object-acl --bucket "${FIREFOX_S3_BUCKET}" --key "\${extensionFilename}" --acl public-read
# url is https://tools.s3.eu-west-1.amazonaws.com/tools-2.5.6-an%2Bfx.xpi
echo "Upload new version as current version"
aws s3 cp "\$(pwd)/packages/\${extensionFilename}" "s3://${FIREFOX_S3_BUCKET}/tools-an+fx.xpi"
aws s3api put-object-acl --bucket "${FIREFOX_S3_BUCKET}" --key "tools-an+fx.xpi" --acl public-read
# url is https://tools.s3.eu-west-1.amazonaws.com/tools-an%2Bfx.xpi
echo "Upload updates.json file"
aws s3 cp "\$(pwd)/packages/updates.json" "s3://${FIREFOX_S3_BUCKET}"
aws s3api put-object-acl --bucket "${FIREFOX_S3_BUCKET}" --key "updates.json" --acl public-read
# url is https://tools.s3.eu-west-1.amazonaws.com/updates.json
"""
git.updateGithubCommitStatus("FIREFOX_DEPLOYED")
}
}
}
}
}
}
}
}
post {
always {
script {
archiveArtifacts artifacts: 'report/mega-linter.log'
archiveArtifacts artifacts: 'report/linters_logs/*'
archiveArtifacts artifacts: 'packages/*', fingerprint: true, allowEmptyArchive: true
// send email to the builder and culprits of the current commit
// culprits are the committers since the last commit successfully built
mail.sendConditionalEmail()
git.updateConditionalGithubCommitStatus()
}
}
success {
script {
if (params.targetStore != 'none' && env.GIT_BRANCH == 'origin/master') {
// send an email to a teams channel so every collaborators knows
// when a production ready extension has been deployed
mail.sendSuccessfulEmail('[email protected]')
}
}
}
}
}
1.9 - Annotated Jenkinsfiles - Part 5
Detailed Jenkinsfile examples with annotations
1. introduction
In jenkins library you can create your own directive that allows to generate jenkinsfile code. Here we will use this
feature to generate a complete Jenkinsfile.
2. Annotated Jenkinsfile
library identifier: '[email protected]',
retriever: modernSCM([
$class: 'GitSCMSource',
remote: '[email protected]:fchastanet/jenkins-library.git',
credentialsId: 'jenkinsCredentialsId'
])
djangoApiPipeline repoUrl: '[email protected]:fchastanet/django_api_project.git',
imageName: 'django_api'
3. Annotated library custom directive
In the jenkins library just add a file named vars/djangoApiPipeline.groovy with the following content
#!/usr/bin/env groovy
def call(Map args) {
// content of your pipeline
}
4. Annotated library custom directive djangoApiPipeline.groovy
#!/usr/bin/env groovy
def call(Map args) {
def gitUtil = new Git(this)
def mailUtil = new Mail(this)
def dockerUtil = new Docker(this)
def kubernetesUtil = new Kubernetes(this)
def testUtil = new Tests(this)
String workerLabelNonProd = args?.workerLabelNonProd ?: 'eks-nonprod'
String workerLabelProd = args?.workerLabelProd ?: 'docker-ubuntu-prod-eks'
String awsRegionNonProd = workerLabelNonProd == 'eks-nonprod' ? 'us-east-1' : 'eu-west-1'
String awsRegionProd = 'eu-central-1'
String regionName = params.targetEnv == 'prod' ? awsRegionProd : awsRegionNonProd
String teamsEmail = args?.teamsEmail ?: '[email protected]'
String helmDirectory = args?.helmDirectory ?: './helm'
Boolean sendCortexMetrics = args?.sendCortexMetrics ?: false
Boolean skipTests = args?.skipTests ?: false
List environments = args?.environments ?: ['none', 'qa', 'prod']
Short skipBuild = 0
pipeline {
agent {
node {
label params.targetEnv == 'prod' ? workerLabelProd : workerLabelNonProd
}
}
parameters {
gitParameter branchFilter: 'origin/(.*)',
defaultValue: 'main',
quickFilterEnabled: true,
sortMode: 'ASCENDING_SMART',
name: 'BRANCH',
type: 'PT_BRANCH'
choice (
name: 'targetEnv',
choices: environments,
description: 'Where it should be deployed to? (Default: none - No deploy)'
)
string (
name: 'instance',
defaultValue: '1',
description: '''The instance ID to define which QA instance it should
be deployed to (Will only apply if targetEnv is qa).'''
)
booleanParam(
name: 'suspendCron',
defaultValue: true,
description: 'Suspend cron jobs scheduling'
)
choice (
name: 'upStreamImage',
choices: ['latest', 'beta'],
description: '''Select beta to check if your build works with the
future version of the upstream image'''
)
}
stages {
stage('Checkout from SCM') {
steps {
script {
echo "Checking out from origin/${BRANCH} branch"
gitUtil.branchCheckout(
'',
'babee6c1-14fe-4d90-9da0-ffa7068c69af',
args.repoUrl,
'${BRANCH}'
)
wrap([$class: 'BuildUser']) {
def String displayName = "#${currentBuild.number}_${BRANCH}_${BUILD_USER}_${targetEnv}"
if (params.targetEnv == 'qa' || params.targetEnv == 'qe') {
displayName = "${displayName}_${instance}"
}
currentBuild.displayName = displayName
}
env.imageName = env.BUILD_TAG.toLowerCase()
env.buildDirectory = args?.buildDirectory ?
args.buildDirectory + "/" : ""
env.runCoverage = args?.runCoverage
env.shortSha = gitUtil.getShortCommitSha(env.GIT_BRANCH)
skipBuild = dockerUtil.checkImage(args.imageName, shortSha)
}
}
}
stage('Build') {
when {
expression { return skipBuild != 0 }
}
steps {
script {
String registryUrl = 'dockerRegistryId.dkr.ecr.' +
awsRegionNonProd + '.amazonaws.com'
String buildDirectory = args?.buildDirectory ?: pwd()
if (params.targetEnv == "prod") {
registryUrl = 'dockerRegistryId.dkr.ecr.' + awsRegionProd + '.amazonaws.com'
}
dockerUtil.pullBuildImage(
registryImageUrl: "${registryUrl}/${args.imageName}",
pullTags: [
"${params.targetEnv}"
],
buildDirectory: "${buildDirectory}",
buildArgs: "--build-arg UPSTREAM_VERSION=${params.upStreamImage}",
tagPrefix: "${env.imageName}:",
tags: [
"${env.shortSha}"
]
)
}
}
}
stage('Test') {
when {
expression { return skipBuild != 0 && skipTests == false }
}
steps {
script {
testUtil.execTests(args.imageName)
}
}
}
stage('Push') {
when {
expression { return params.targetEnv != 'none' }
}
steps {
script {
//pipeline execution starting time for CD part
Map argsMap = [:]
if (params.targetEnv == "prod") {
registryUrl = 'registryIdProd.dkr.ecr.' +
awsRegionProd + '.amazonaws.com'
} else {
registryUrl = 'registryIdNonProd.dkr.ecr.' +
awsRegionNonProd + '.amazonaws.com'
}
argsMap = [
registryImageUrl: "${registryUrl}/${args.imageName}",
pullTags: [
"${env.shortSha}",
],
tagPrefix: "${registryUrl}/${args.imageName}:",
localTagName: "${env.shortSha}",
tags: [
"${params.targetEnv}"
]
]
if (skipBuild == 0) {
dockerUtil.promoteTag(argsMap)
} else {
argsMap.remove("pullTags")
argsMap.put("tagPrefix", "${env.imageName}:")
argsMap.put("tags", ["${env.shortSha}","${params.targetEnv}"])
dockerUtil.tagPushImage(argsMap)
}
}
}
}
stage("Deploy to Kubernetes") {
when {
expression { return params.targetEnv != 'none' }
}
steps {
script {
if (params.targetEnv == 'prod') {
// not sure it is a good practice as it forces the operator to
// wait for build to reach this stage
timeout(time: 300, unit: "SECONDS") {
input(
message: """Do you want go ahead with ${env.shortSha}
image tag for prod helm deploy?""",
ok: 'Yes'
)
}
}
CHART_NAME = (args.imageName).contains("_") ?
(args.imageName).replaceAll("_", "-") :
(args.imageName)
if (params.targetEnv == 'qa' || params.targetEnv == 'qe') {
helmValueFilePath = "${helmDirectory}" +
"/value_files/values-" + params.targetEnv +
params.instance + ".yaml"
NAMESPACE = "${CHART_NAME}-" + params.targetEnv + params.instance
} else {
helmValueFilePath = "${helmDirectory}" +
"/value_files/values-" + params.targetEnv + ".yaml"
NAMESPACE = "${CHART_NAME}-" + params.targetEnv
}
ingressUrl = kubernetesUtil.getIngressUrl(helmValueFilePath)
echo "Deploying into k8s.."
echo "Helm release: ${CHART_NAME}"
echo "Target env: ${params.targetEnv}"
echo "Url: ${ingressUrl}"
echo "K8s namespace: ${NAMESPACE}"
kubernetesUtil.deployHelmChart(
chartName: CHART_NAME,
nameSpace: NAMESPACE,
imageTag: "${env.shortSha}",
helmDirectory: "${helmDirectory}",
helmValueFilePath: helmValueFilePath
)
}
}
}
}
post {
always {
script {
gitUtil.updateGithubCommitStatus("${currentBuild.currentResult}", "${env.WORKSPACE}")
mailUtil.sendConditionalEmail()
if (params.targetEnv == 'prod') {
mailUtil.sendTeamsNotification(teamsEmail)
}
}
}
}
}
}
5. Final thoughts about this technique
This technique is really useful when you have a lot of similar projects reusing over and over the same pipeline. It
allows:
- code reuse
- avoid duplicated code
- easier maintenance
However it has the following drawbacks:
- some projects using this generic pipeline could have specific needs
- eg 1: not the same way to run unit tests, to overcome that issue the method
testUtil.execTests is used allowing to
run a specific sh file if it exists - eg 2: more complex way to launch docker environment
- …
- be careful, when you upgrade this jenkinsfile as all the projects using it will be upgraded at once
- it could be seen as an advantage, but it is also a big risk as it could impact all the prod environment at once
- to overcome that issue I suggest to use library versioning when using the jenkins library in your project pipeline
Eg: check Annotated Jenkinsfile
@v1.0 when cloning library project
- I highly suggest to use a unit test framework of the library to avoid at most bad surprises
In conclusion, I’m still not sure it is a best practice to generate pipelines like this.
1.10 - Jenkins Recipes and Tips
Useful recipes and tips for Jenkins and Jenkinsfiles
1. Jenkins snippet generator
Use jenkins snippet generator by adding /pipeline-syntax/ to your jenkins pipeline. to allow you to generate jenkins
pipeline code easily with inline doc. It also list the available variables.

2. Declarative pipeline allows you to restart a build from a given stage

3. Replay a pipeline
Replaying a pipeline allows you to update your jenkinsfile before replaying the pipeline, easier debugging !

4. VS code Jenkinsfile validation
Please follow this documentation
enable jenkins pipeline linter in vscode
5. How to chain pipelines ?
Simply use the build directive followed by the name of the build to launch
6. Viewing pipelines hierarchy
The downstream-buildview plugin allows to view the full chain of
dependent builds.

2 - How to Write PlantUML
Comprehensive guide to writing PlantUML diagrams, including syntax, best practices, and examples.
1. What You’ll Learn
2. Getting Started
Select a guide from the sidebar to begin.
2.1 - Reusable PlantUML Components: Modular Diagram Architecture and Shared Styling
Learn how to create reusable PlantUML components for modular diagram architecture and shared styling across multiple diagrams.
Today, we will explore how to create reusable PlantUML components for modular diagram architecture and shared styling
across multiple diagrams. This approach allows you to maintain consistency and reduce duplication in your PlantUML
diagrams.
1. Database ERD Examples: Music Domain
The database/ directory contains a complete example of MongoDB Entity Relationship Diagrams modeling a music
streaming recommendation system. These examples demonstrate:
- Modular diagram architecture with reusable components
- Subsection inclusion using
!includesub and !startsub - Shared styling across multiple diagrams
- JSON data structures within PlantUML diagrams
1.1. Entity Collections
The music database example includes the following collections:
| File | Description |
|---|
music-db-Playlists_Collections_ERD.puml | Playlist catalog and published playlist instances |
music-db-Moods_Collections_ERD.puml | Music mood taxonomy and user taste preferences |
music-db-Suggestions_Collections_ERD.puml | Playlist suggestion engine based on user moods |
music-db-Logs.puml | API usage logs for AI-generated playlist metadata |
music-db-All_collections.puml | Master diagram combining all collections |
db_theme_standard.puml | Reusable theme providing consistent styling |
1.2. Business Logic Flow
- Playlists are tagged with moods (e.g., “energetic”, “chill”, “melancholic”)
- Users express music preferences through user_tastes (preferred moods)
- The suggestion engine matches playlists to users based on mood similarity
- Vector embeddings enable semantic matching between user tastes and playlist moods
1.3. Complete Database Schema Overview
The following diagram shows all collections and their relationships:

View source on GitHub
View PlantUML source code
@startuml
!pragma layout smetana
!include content/docs/howtos/how-to-write-plantuml/database/db_theme_standard.puml
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_MODEL
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_MODEL_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_INSTANCES_MODEL
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_INSTANCES_MODEL_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_USER_TASTES
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_USER_TASTES_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_MOOD
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_MOOD_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Suggestions_Collections_ERD.puml!MODEL_PENDING_USER_SUGGESTION
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Suggestions_Collections_ERD.puml!MODEL_PENDING_USER_SUGGESTION_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Suggestions_Collections_ERD.puml!MODEL_USER_SUGGESTION
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Suggestions_Collections_ERD.puml!MODEL_USER_SUGGESTION_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Logs.puml!MODEL_MUSIC_API_LOGS
@enduml
2. Reusable Styling with db_theme_standard.puml
The db_theme_standard.puml file is a standalone, reusable theme definition that ensures visual consistency across
all database diagrams.
2.1. Why Separate Styling?
Separating styling from content provides several benefits:
- Single source of truth for visual standards
- Easy updates - change once, apply everywhere
- Reduced duplication - no need to copy/paste styling rules
- Clear separation between diagram content and presentation
2.2. What It Defines
The reusable theme file defines:
!include content/docs/howtos/how-to-write-plantuml/database/db_theme_standard.puml
- Visual styling: fonts, colors, line styles, rounded corners
- Notation macros:
PK (primary key), FK (foreign key), IDX (index) - Index types:
UNIQUE, SPARSE, UNIQUE_SPARSE - Legend symbols: explaining all notation used in diagrams
Here’s what the theme styling looks like:

View source on GitHub
View theme source code
@startuml
' ==============================================
' MongoDB Database Diagram Standard Theme
' ==============================================
' This is a REUSABLE style definition file that provides:
' - Consistent visual styling across all database diagrams
' - Standard notation for database elements (PK, FK, indexes)
' - Color-coded legends for different entity types
' - Icon definitions for database constraints
'
' USAGE: Include this file in any database ERD diagram:
' !include path/to/db_theme_standard.puml
'
' This ensures all diagrams share the same professional appearance
' and use consistent notation conventions.
' ==============================================
skinparam {
defaultFontName Arial
defaultFontSize 12
roundCorner 8
packageStyle rectangle
linetype ortho
BackgroundColor #FFF
shadowing false
ArrowColor #555555
ArrowThickness 2
entity {
Margin 20
}
}
!define ENTITY entity
!define PK <&key><u><b>
!define FK <u><i>
!define IDX <&magnifying-glass>
!define UNIQUE <u><&magnifying-glass><<unique>>
!define SPARSE <i><&magnifying-glass><<sparse>>
!define UNIQUE_SPARSE <u><i><&magnifying-glass><<unique_sparse>>
!define DETAILS -
legend
|= |= Type |
|<back:#FF0000> </back>| Type A class |
|<back:#00FF00> </back>| Type B class |
|<back:blue> </back>| Type C class |
endlegend
legend left
|= notation |= meaning|
| <img : https://cdn-0.plantuml.com/public-field.png{scale=1.4}>(+) | Indexed column |
| <img : https://cdn-0.plantuml.com/private-field.png{scale=1.4}>(-) | details |
| ""<&key>"" | Primary Key |
| ""<u><i>column"" | Foreign Key |
| ""<&magnifying-glass>"" | Standard Index |
| UNIQUE | Unique Index |
| SPARSE | Sparse Index |
| UNIQUE_SPARSE | Unique & Sparse Index |
endlegend
@enduml
2.3. Usage Pattern
Every database ERD diagram includes this file:
@startuml
!include content/docs/howtos/how-to-write-plantuml/database/db_theme_standard.puml
ENTITY users {
PK _id : ObjectId
--
+ IDX email : string
}
@enduml
This pattern ensures all diagrams share professional, consistent notation.
3. Modular Composition with !startsub and !includesub
PlantUML supports subsection extraction, allowing you to define reusable diagram fragments that can be included
selectively in other diagrams.
3.1. Defining Subsections: !startsub
Use !startsub and !endsub to mark reusable sections:
' Define a reusable playlist entity
!startsub PLAYLIST_MODEL
ENTITY playlists {
PK _id : ObjectId
+ IDX reference_code : string
}
!endsub
' Define additional detail fields
!startsub PLAYLIST_MODEL_DETAILS
ENTITY playlists {
# moods : ObjectId[]
# title : string
- created_at : datetime
}
!endsub
3.2. Including Subsections: !includesub
Use !includesub to import specific subsections into another diagram:
@startuml
!include db_theme_standard.puml
' Include only the core playlist model (without details)
!includesub music-db-Playlists_Collections_ERD.puml!PLAYLIST_MODEL
@enduml
3.3. Benefits of Subsection Inclusion
- Selective composition - include only what you need
- Multiple levels of detail - show high-level or detailed views
- Avoid duplication - define entities once, reuse everywhere
- Maintainability - update the source, all diagrams reflect changes
3.4. Example: Cross-Diagram References
In music-db-Moods_Collections_ERD.puml:
package "Playlist Collections" <<Only the relevant fields are shown>> #LightGray {
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_MODEL
}
This imports the playlists entity definition without duplicating code, showing how moods relate to playlists.
3.4.1. Example Diagrams
Playlists Collections:

View source on GitHub
View PlantUML source code
@startuml Playlists Collections - Entity Relationship Diagram
title MongoDB Collections: playlist_instances and playlists
!include content/docs/howtos/how-to-write-plantuml/database/db_theme_standard.puml
' PLAYLIST_MODEL
!startsub PLAYLIST_MODEL
ENTITY playlists {
PK _id : ObjectId
--
+ IDX reference_code : string
+ IDX is_featured : bool
+ IDX must_tag_moods_at : datetime|null
}
!endsub
!startsub PLAYLIST_MODEL_DETAILS
ENTITY playlists {
# moods : ObjectId[]
# locale : string
# title : string
# description : string
# genre : string
# theme : string
# target_audience : string
# moods_tagged_at : datetime|null
- created_at : datetime
- updated_at : datetime
__Indexes__
SPARSE is_featured_reference_code_idx(is_featured ASC, reference_code ASC)
SPARSE must_tag_moods_at_idx (must_tag_moods_at ASC)
}
!endsub
' PLAYLIST_INSTANCES_MODEL
!startsub PLAYLIST_INSTANCES_MODEL
ENTITY playlist_instances {
PK _id : ObjectId
--
+ IDX instance_urn : string
+ IDX playlist : Link[playlists]
+ IDX playlist_urn : string
+ IDX has_moods : bool
+ IDX is_published : bool
}
' Relationships
playlist_instances ||--o{ playlists : "playlist_id"
!endsub
!startsub PLAYLIST_INSTANCES_MODEL_DETAILS
ENTITY playlist_instances {
uuid : UUID
is_moods_based_suggestion : bool
-created_at : datetime
-updated_at : datetime
__Indexes__
IDX playlist_id_idx(playlist.$id ASC)
IDX instance_urn_has_moods_is_published_idx(instance_urn ASC, has_moods ASC, is_published ASC)
UNIQUE playlist_urn_instance_urn_unique_idx(playlist_urn ASC, instance_urn ASC)
}
!endsub
note right of playlists::is_featured_reference_code_idx
**Index Purpose:**
Contains playlist data from various music streaming
platforms. Serves as main source for user suggestions.
end note
note right of playlist_instances::playlist_urn_instance_urn_unique_idx
**Index Purpose:**
Featured playlists catalogue with shared
information across multiple playlist versions.
Master template for mood tagging reference.
end note
note bottom of playlists
**Relationship Pattern:**
One featured_playlist can be associated with multiple playlist records
playlist_instances.playlist.$id → playlist._id
end note
@enduml
Moods Collections:

View source on GitHub
View PlantUML source code
@startuml Moods Collections - Entity Relationship Diagram
title MongoDB Collections: Moods taxonomy and related collections - Entity Relationship Diagram
!include content/docs/howtos/how-to-write-plantuml/database/db_theme_standard.puml
package "Playlist Collections" <<Only the relevant fields are shown>> #LightGray {
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_MODEL
}
!startsub MODEL_USER_TASTES
ENTITY user_tastes {
PK _id : ObjectId
--
+ user_urn : string
+ instance_urn : string
+ has_moods : boolean
moods_ids : ObjectId[]
}
!endsub
!startsub MODEL_USER_TASTES_DETAILS
ENTITY user_tastes {
DETAILS created_at : datetime
DETAILS updated_at : datetime
__Indexes__
UNIQUE_SPARSE user_urn_unique_idx(user_urn ASC)
SPARSE instance_urn_has_moods_idx(instance_urn ASC, has_moods DESC)
}
!endsub
!startsub MODEL_MOOD
ENTITY mood {
PK _id : ObjectId
--
+ name : string
+ vector: float[]
+ vector_cohere: float[]
+ translations : dict<string, string>
}
!endsub
!startsub MODEL_MOOD_DETAILS
ENTITY mood {
__Indexes__
UNIQUE name_unique_idx(name ASC)
SPARSE vector_exists_idx(vector ASC)
}
' Relationships
mood }o--o{ user_tastes : "moods_ids references _id in mood"
mood }o-r-o{ playlists : "moods references _id in mood"
!endsub
note left of mood::name
The name of the mood in English
(e.g., "energetic", "chill", "melancholic")
end note
note left of mood::translations
A dictionary containing translations
of the mood name in various languages.
The keys are BCP-47 language (e.g., "fr" for French)
and the values are the translated mood names.
end note
note right of mood::vector
A vector representation of the mood.name field,
generated using amazon.titan-embed-text-v1.
This field is used for semantic search and
similarity comparisons between moods.
end note
@enduml
Suggestions Collections:

View source on GitHub
View PlantUML source code
@startuml Suggestions Collections - Entity Relationship Diagram
title MongoDB Collections: Suggestion System - Entity Relationship Diagram
!include content/docs/howtos/how-to-write-plantuml/database/db_theme_standard.puml
package "Playlist Collections" <<Only the relevant fields are shown>> as PC #LightGray {
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_MODEL
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_INSTANCES_MODEL
}
package "User Collections" <<Only the relevant fields are shown>> as UC #LightGray{
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_USER_TASTES
}
!startsub MODEL_PENDING_USER_SUGGESTION
ENTITY pending_user_suggestion {
PK _id : ObjectId
--
+ IDX user_tastes_id: ObjectId
+ IDX playlist_instances_id : ObjectId
+ IDX computed_at : datetime
user_urn : string
user_moods : ObjectId[]
playlist_urn : string
playlist_uuid : UUID
playlist_moods : ObjectId[]
playlist_instance_is_suggestible : bool
instance_urn : string
}
!endsub
!startsub MODEL_PENDING_USER_SUGGESTION_DETAILS
ENTITY pending_user_suggestion {
__Indexes__
UNIQUE user_tastes_playlist_instances_unique_idx(user_tastes_id ASC, playlist_instances_id ASC)
IDX compute_at_idx(computed_at ASC)
IDX playlist_instances_idx(playlist_instances_id ASC)
}
!endsub
!startsub MODEL_USER_SUGGESTION
ENTITY user_suggestion {
PK _id : ObjectId
--
+user_tastes_id: ObjectId
+user_urn : string
+playlist_instances_id : ObjectId
+playlist_instance_is_suggestible : bool
+score : float
playlist_urn : string
playlist_uuid : UUID
instance_urn : string
computed_at : datetime
}
' Relationships
user_tastes ||--o{ pending_user_suggestion
playlist_instances ||--o{ pending_user_suggestion
user_tastes ||-d-o{ user_suggestion
playlist_instances ||-u-o{ user_suggestion
!endsub
!startsub MODEL_USER_SUGGESTION_DETAILS
ENTITY user_suggestion {
__Indexes__
UNIQUE user_tastes_playlist_instances_unique_idx(user_tastes_id ASC, playlist_instances_id ASC)
IDX user_tastes_score_idx(user_tastes_id ASC, score DESC)
IDX playlist_instances_idx(playlist_instances_id ASC)
IDX user_urn_is_suggestible_score_idx(user_urn ASC, playlist_instance_is_suggestible DESC, score DESC)
}
!endsub
@enduml
4. Master Diagram: music-db-All_collections.puml
The music-db-All_collections.puml file demonstrates diagram composition by
combining all subsections into a complete database schema view.
4.1. How It Works
@startuml
!pragma layout smetana
!include content/docs/howtos/how-to-write-plantuml/database/db_theme_standard.puml
' Include playlist entities
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_MODEL
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_MODEL_DETAILS
' Include mood entities
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_MOOD
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_MOOD_DETAILS
' ... and so on for all collections
@enduml
4.2. Advantages
- Single comprehensive view of the entire database
- No code duplication - entities defined once in source files
- Automatic updates - changes propagate from source diagrams
- Flexible composition - easily add/remove collections
This pattern is perfect for creating both detailed individual diagrams and high-level overview diagrams from the
same source.
4.2.1. Master Diagram View

View source on GitHub
View complete PlantUML source code
@startuml
!pragma layout smetana
!include content/docs/howtos/how-to-write-plantuml/database/db_theme_standard.puml
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_MODEL
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_MODEL_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_INSTANCES_MODEL
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Playlists_Collections_ERD.puml!PLAYLIST_INSTANCES_MODEL_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_USER_TASTES
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_USER_TASTES_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_MOOD
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Moods_Collections_ERD.puml!MODEL_MOOD_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Suggestions_Collections_ERD.puml!MODEL_PENDING_USER_SUGGESTION
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Suggestions_Collections_ERD.puml!MODEL_PENDING_USER_SUGGESTION_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Suggestions_Collections_ERD.puml!MODEL_USER_SUGGESTION
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Suggestions_Collections_ERD.puml!MODEL_USER_SUGGESTION_DETAILS
!includesub content/docs/howtos/how-to-write-plantuml/database/music-db-Logs.puml!MODEL_MUSIC_API_LOGS
@enduml
5. JSON Data Structures in PlantUML
PlantUML supports embedding JSON notation directly in diagrams, useful for documenting:
- API request/response structures
- Database document schemas (MongoDB)
- Configuration examples
- Data transformation flows
5.1. Example from music-db-Logs.puml
json payload as "**Payload Example**" {
"**field**": "title",
"**value**": "Summer Vibes Mix",
"**fallback_locale**": "en-US"
}
music_api_logs -r-> payload: payload
This creates a formatted JSON box linked to an entity, showing exactly what data structure is used.
5.2. Complete Document Example
The logs diagram also shows a complete MongoDB document:
json complete_example as "**Complete Document Example**" {
"**_id**": "698ddd946f3bad1915a67e87",
"**instance_urn**": "urn:music:spotify",
"**user_urn**": "urn:music:spotify:user/...",
"**tag**": "music-ai.playlist_metadata.generate",
"**payload**": {
"**field**": "description",
"**playlist_title**": "Acoustic Coffee House",
"**additional_metadata**": { "..." }
},
"**prediction**": ["..."]
}
5.3. Benefits
- Precise schema documentation alongside ERD diagrams
- Visual clarity - readers see exact data structures
- Version control - schema examples tracked with diagrams
- Testing reference - developers can use examples for test data
5.4. Complete Logs Diagram Example
Here’s the complete logs diagram showing JSON structures and their relationships:

View source on GitHub
View PlantUML source code
@startuml
title MongoDB Collections: Music API Logs - Entity Relationship Diagram
!include content/docs/howtos/how-to-write-plantuml/database/db_theme_standard.puml
!startsub MODEL_MUSIC_API_LOGS
ENTITY music_api_logs {
PK _id : ObjectId
--
+ instance_urn : string
user_urn : string
+ created_at : datetime
tag : string
target_field : string
payload : dict
prediction : array
}
json payload_schema as "**Payload Schema**" {
"**field**": "enum(description|theme|target_audience)",
"**playlist_title**": "string",
"**fallback_locale**": "en-US",
"**additional_metadata**": {
"**description**": "string",
"**theme**": "string",
"**target_audience**": "string"
},
"**example**": "string"
}
music_api_logs --> payload_schema: payload schema
!endsub
!startsub MODEL_MUSIC_API_LOGS_DETAILS
ENTITY music_api_logs {
__Indexes__
IDX ttl_idx_90(created_at ASC)
IDX instance_urn_created_at_idx(instance_urn ASC, created_at DESC)
}
note left of music_api_logs::ttl_idx_90
**Index Purpose:**
Automatically delete documents after a certain
period (e.g., 90 days) to manage storage and
ensure data relevance.
**expireAfterSeconds:** 7776000 (90 days)
end note
json payload as "**Payload Example**" {
"**field**": "title",
"**value**": "Summer Vibes Mix",
"**fallback_locale**": "en-US"
}
music_api_logs -r-> payload: payload
json prediction as "**Prediction Example**" {
[
"Feel-Good Summer Playlist",
"Sunshine & Good Times",
"Ultimate Summer Anthems"
]
}
music_api_logs -d-> prediction: prediction
json complete_example as "**Complete Document Example**" {
"**_id**": "698ddd946f3bad1915a67e87",
"**instance_urn**": "urn:music:spotify",
"**user_urn**": "urn:music:spotify:user/DB991B65-1B6F-A942-32BD-4558C0ED7AF4",
"**tag**": "music-ai.playlist_metadata.generate",
"**target_field**": "description",
"**payload**": {
"**field**": "description",
"**playlist_title**": "Acoustic Coffee House",
"**fallback_locale**": "en-US",
"**additional_metadata**": {
"**description**": "Warm acoustic sounds perfect for your morning coffee. Featuring indie folk, singer-songwriter gems **(truncated...)**",
"**theme**": null,
"**target_audience**": null
},
"**example**": "ex: Discover fresh indie folk tracks to elevate your coffee ritual **(truncated...)**"
},
"**prediction**": [
"Cozy acoustic melodies and indie folk favorites to accompany your coffee break **(truncated...)**"
],
"**created_at**": "2026-02-12T14:03:00.928Z"
}
music_api_logs --> complete_example
!endsub
@enduml
3 - How to Write Dockerfiles
Best practices for writing efficient and secure Dockerfiles
1. Dockerfile best practices
Follow official best practices and you can
follow these specific best practices
But The worst so-called “best practice” for Docker
Backup, explains why you should actually also use
apt-get upgrade
Use hadolint
Use ;\ to separate each command line
- some Dockerfiles are using
&& to separate commands in the same RUN instruction (I was doing it too ;-), but I
strongly discourage it because it breaks the checks done by set -o errexit set -o errexit makes the whole RUN instruction to fail if one of the commands has failed, but it is not the same
when using &&
One package by line, packages sorted alphabetically to ease readability and merges
Always specify the most exact version possible of your packages (to avoid to get major version that would break your
build or software)
do not usage docker image with latest tag, always specify the right version to use
2. Basic best practices
2.1. Best Practice #1: Merge the image layers
in a Dockerfile each RUN command will create an image layer.
2.1.1. Bad practice #1
Here a bad practice that you shouldn’t follow

2.1.2. Best practice #1
Best practice #1 merge the RUN layers to avoid cache issue and gain on total image size
FROM ubuntu:20.04
RUN apt-get update \
&& apt-get install -y apache2 \
&& rm -rf \
/var/lib/apt/lists/* \
/tmp/* \
/var/tmp/* \
/usr/share/doc/*
2.2. Best Practice #2: trace commands and fail on error
from previous example we want to trace each command that is executed
2.2.1. Bad practice #2
when building complex layer and one of the command fails, it’s interesting to know which command makes the build to fail
FROM ubuntu:20.04
RUN apt-get update \
&& [ -d badFolder ] \
&& apt-get install -y apache2 \
&& rm -rf \
/var/lib/apt/lists/* \
/tmp/* \
/var/tmp/* \
/usr/share/doc/*
docker build . gives the following log output(partly truncated):
...
#5 [2/2] RUN apt-get update
&& [ -d badFolder ]
&& apt-get install -y apache2
&& rm -rf
/var/lib/apt/lists/*
/tmp/*
/var/tmp/*
/usr/share/doc/*
#5 3.818 Get:1 http://archive.ubuntu.com/ubuntu focal InRelease [265 kB]
...
#5 6.252 Fetched 25.6 MB in 6s (4417 kB/s)
#5 6.252 Reading package lists...
#5 ERROR: process "/bin/sh -c apt-get update
&& [ -d badFolder ]
&& apt-get install -y apache2
&& rm -rf
/var/lib/apt/lists/*
/tmp/*
/var/tmp/*
/usr/share/doc/*"
did not complete successfully: exit code: 1
------
> [2/2] RUN apt-get update
&& [ -d badFolder ]
&& apt-get install -y apache2
&& rm -rf
/var/lib/apt/lists/*
/tmp/*
/var/tmp/*
/usr/share/doc/*:
#5 5.383 Get:10 http://archive.ubuntu.com/ubuntu focal/main amd64 Packages [1275 kB]
...
------
Dockerfile1:3
--------------------
2 |
3 | >>> RUN apt-get update \
4 | >>> && [ -d badFolder ] \
5 | >>> && apt-get install -y apache2 \
6 | >>> && rm -rf \
7 | >>> /var/lib/apt/lists/\* \
8 | >>> /tmp/\* \
9 | >>> /var/tmp/\* \
10 | >>> /usr/share/doc/\*
11 |
--------------------
ERROR: failed to solve: process "/bin/sh -c apt-get update
&& [ -d badFolder ]
&& apt-get install -y apache2
&& rm -rf
/var/lib/apt/lists/*
/tmp/*
/var/tmp/*
/usr/share/doc/*
did not complete successfully: exit code: 1
Not easy here to know that the command [ -d badFolder ] makes the build failing
Without the best practice #2, the following code build successfully
FROM ubuntu:20.04
RUN set -x ;\
apt-get update ;\
[ -d badFolder ] ;\
ls -al
2.2.2. Best Practice #2
Best Practice #2: Override SHELL options of the RUN command and use ;\ instead of &&
The following options are set on the shell to override the default behavior:
set -o pipefail: The return status of a pipeline is the exit status of the last command, unless the pipefail option
is enabled.- If pipefail is enabled, the pipeline’s return status is the value of the last (rightmost) command to exit with a
non-zero status, or zero if all commands exit successfully.
- without it, a command failure could be masked by the command piped after it
set -o errexit (same as set -e): Exit immediately if a pipeline (which may consist of a single simple command), a
list, or a compound command (see SHELL GRAMMAR above), exits with a non-zero status.set -o xtrace(same as set -x): After expanding each simple command, for command, case command, select
command, or arithmetic for command, display the expanded value of PS4, followed by the command and its expanded
arguments or associated word list.
Those options are not mandatory but are strongly advised. Although there are some workaround to know:
- if a command can fail and you want to ignore it, you can use
- commandThatCanFail || true
These options can be used with /bin/sh as well.
Also it is strongly advised to use ;\ to separate commands because it could happen that some errors are ignored when
&& is used in conjunction with ||
FROM ubuntu:20.04
# The SHELL instructions will be applied to all the subsequent RUN instructions
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
RUN apt-get update ;\
[ -d badFolder ] ;\
apt-get install -y apache2 ;\
rm -rf \
/var/lib/apt/lists/* \
/tmp/* \
/var/tmp/* \
/usr/share/doc/*
docker build . gives the following log output(partly truncated):
...
#5 [2/2] RUN apt-get update ;
[ -d badFolder ] ;
apt-get install -y apache2 ;
rm -rf
/var/lib/apt/lists/*
/tmp/*
/var/tmp/*
/usr/share/doc/*
#5 0.318 + apt-get update
#5 3.522 Get:1 http://archive.ubuntu.com/ubuntu focal InRelease [265 kB]
...
#5 5.310 Fetched 25.6 MB in 5s (5141 kB/s)
#5 5.310 Reading package lists...
#5 6.172 + '[' -d badFolder ']'
#5 ERROR: process "/bin/bash -o pipefail -o errexit -o xtrace -c
apt-get update ;
[ -d badFolder ] ;
apt-get install -y apache2 ;
rm -rf
/var/lib/apt/lists/*
/tmp/*
/var/tmp/*
/usr/share/doc/*
did not complete successfully: exit code: 1
------
> [2/2] RUN apt-get update ;
[ -d badFolder ] ;
apt-get install -y apache2 ;
rm -rf
/var/lib/apt/lists/*
/tmp/*
/var/tmp/*
/usr/share/doc/\*:
#5 4.228 Get:11 http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages [3014 kB]
...
#5 6.172 + '[' -d badFolder ']'
------
Dockerfile1:4
--------------------
3 | SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
4 | >>> RUN apt-get update ;\
5 | >>> [ -d badFolder ] ;\
6 | >>> apt-get install -y apache2 ;\
7 | >>> rm -rf \
8 | >>> /var/lib/apt/lists/\* \
9 | >>> /tmp/\* \
10 | >>> /var/tmp/\* \
11 | >>> /usr/share/doc/\*
12 |
--------------------
ERROR: failed to solve: process "/bin/bash -o pipefail -o errexit -o xtrace -c
apt-get update ; [ -d badFolder ] ; apt-get install -y apache2 ; rm -rf
/var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*"
did not complete successfully: exit code: 1
Here the command line displayed just above the error indicates clearly from where the error comes from:
#5 6.172 + '[' -d badFolder ']'
2.3. Best practice #3: packages ordering and versions
Best Practice #3: order packages alphabetically, always specify packages versions, ensure non interactive
From previous example we want to install several packages
2.3.1. Bad practice #3
let’s add some packages on our previous example (errors removed)
The following docker has the following issues:
- it doesn’t set the package versions
- the installation will install also the recommended packages
- it’s using apt instead of apt-get (hadolint warning DL3027 Do not
use apt as it is meant to be a end-user tool, use
apt-get or apt-cache instead) - the packages are not ordered alphabetically
FROM ubuntu:20.04
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
RUN apt update ;\
apt install -y php7.4 apache2 php7.4-curl redis-tools ;\
rm -rf \
/var/lib/apt/lists/* \
/tmp/* \
/var/tmp/* \
/usr/share/doc/*
2.3.2. Best Practice #3
Best Practice #3: order packages alphabetically, always specify packages versions, ensure non interactive
2.3.2.1. Order packages alphabetically and one package by line
one package by line allows packages to be simpler ordered alphabetically
one package by line and ordering alphabetically allows :
- to merge branches changes more easily
- to detect redundancies more easily
- to improve readability
2.3.2.2. Always specify packages versions
over the time your build’s dependencies could be updated on the remote repositories and your packages be unattended
upgraded to the latest version making your software breaks because it doesn’t manage the changes of the new package.
It happens several times for me, for example, in 2021, xdebug has been automatically upgraded on one of my docker image
from version 2.8 to 3.0 making all the dev environments broken. It happens also on a build pipeline with a version of
npm gulp that has been upgraded to latest version. In both cases we resolved the issue by downgrading the version to the
one we were using.
2.3.2.3. Ensure non interactive
some apt-get packages could ask for interactive questions, you can avoid this using the env variable
DEBIAN_FRONTEND=noninteractive
Note: ARG instruction allows to set env variable available only during build time
FROM ubuntu:20.04
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update ;\
apt-get install -y -q --no-install-recommends \
# Mind to use quotes to avoid shell to try to expand * with some files
apache2='2.4.*' \
php7.4='7.4.*' \
php7.4-curl='7.4.*' \
# Notice the ':'(colon)
redis-tools='5:5.*' \
;\
# cleaning
apt-get autoremove -y ;\
apt-get -y clean ;\
rm -rf \
/var/lib/apt/lists/* \
/tmp/* \
/var/tmp/* \
/usr/share/doc/*
# use the following command to know the current version of the packages
# using another RUN instead of using previous one will avoid the whole
# previous layer to be rebuilt
# RUN apt-cache policy \
# apache2 \
# php7.4 \
# php7.4-curl \
# redis-tools
# Gives the following output
#6 0.387 + apt-cache policy apache2
#6 0.399 apache2:
#6 0.399 Installed: 2.4.41-4ubuntu3.14
#6 0.399 Candidate: 2.4.41-4ubuntu3.14
#6 0.399 Version table:
#6 0.399 *** 2.4.41-4ubuntu3.14 100
#6 0.399 100 /var/lib/dpkg/status
#6 0.400 + apt-cache policy php7.4
#6 0.409 php7.4:
#6 0.409 Installed: 7.4.3-4ubuntu2.18
#6 0.409 Candidate: 7.4.3-4ubuntu2.18
#6 0.409 Version table:
#6 0.409 *** 7.4.3-4ubuntu2.18 100
#6 0.409 100 /var/lib/dpkg/status
#6 0.409 + apt-cache policy php7.4-curl
#6 0.420 php7.4-curl:
#6 0.420 Installed: 7.4.3-4ubuntu2.18
#6 0.420 Candidate: 7.4.3-4ubuntu2.18
#6 0.420 Version table:
#6 0.420 *** 7.4.3-4ubuntu2.18 100
#6 0.421 100 /var/lib/dpkg/status
#6 0.421 + apt-cache policy redis-tools
#6 0.431 redis-tools:
#6 0.431 Installed: 5:5.0.7-2ubuntu0.1
#6 0.431 Candidate: 5:5.0.7-2ubuntu0.1
#6 0.431 Version table:
#6 0.431 *** 5:5.0.7-2ubuntu0.1 100
#6 0.432 100 /var/lib/dpkg/status
2.4. Best practice #4: ensure image receives latest security updates
from previous example we want to ensure the image receives the latest security updates
2.4.1. Bad practice #4
registry image are not always updated and latest apt security updates are not installed
FROM ubuntu:20.04
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update ;\
apt-get install -y -q --no-install-recommends \
apache2='2.4.*' \
php7.4='7.4.*' \
php7.4-curl='7.4.*' \
redis-tools='5:5.*' \
;\
# cleaning
apt-get autoremove -y ;\
apt-get -y clean ;\
rm -rf \
/var/lib/apt/lists/* \
/tmp/* \
/var/tmp/* \
/usr/share/doc/*
2.4.2. Best Practice #4
be sure to apply latest security updates, to install the
latest security updates in the image, keep sure to call apt-get upgrade -y
Here the updated Dockerfile:
FROM ubuntu:20.04
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update ;\
# be sure to apply latest security updates
# https://pythonspeed.com/articles/security-updates-in-docker/
apt-get upgrade -y ;\
apt-get install -y -q --no-install-recommends \
apache2='2.4.*' \
php7.4='7.4.*' \
php7.4-curl='7.4.*' \
redis-tools='5:5.*' \
;\
# cleaning
apt-get autoremove -y ;\
apt-get -y clean ;\
rm -rf \
/var/lib/apt/lists/* \
/tmp/* \
/var/tmp/* \
/usr/share/doc/*
2.5. Conclusion: image size comparison
from previous example we want to ensure the image receives the latest security updates
2.5.1. Dockerfile without best practices
FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get install -y apache2 php7.4 php7.4-curl redis-tools
# cleaning
RUN apt-get autoremove -y ;\
apt-get -y clean ;\
rm -rf \
/var/lib/apt/lists/* \
/tmp/* \
/var/tmp/* \
/usr/share/doc/*
2.5.2. Dockerfile with all optimizations
FROM ubuntu:20.04
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update ;\
apt-get upgrade -y ;\
apt-get install -y -q --no-install-recommends \
apache2='2.4.*' \
php7.4='7.4.*' \
php7.4-curl='7.4.*' \
redis-tools='5:5.*' \
;\
# cleaning
apt-get autoremove -y ;\
apt-get -y clean ;\
rm -rf \
/var/lib/apt/lists/* \
/tmp/* \
/var/tmp/* \
/usr/share/doc/*
3. Docker Buildx best practices
3.1. Optimize image size
Source:
https://askubuntu.com/questions/628407/removing-man-pages-on-ubuntu-docker-installation
Let’s consider this example
3.1.1. Dockerfile not optimized
FROM ubuntu:20.04 as stage1
ARG DEBIAN_FRONTEND=noninteractive
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
RUN \
apt-get update ;\
apt-get install -y -q --no-install-recommends \
htop
FROM stage1 as stage2
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
RUN \
# here we just test that the ARG DEBIAN_FRONTEND has been inherited from
# previous stage (it is the case)
echo "DEBIAN_FRONTEND=${DEBIAN_FRONTEND}"
Now let’s build and check the image size, the best way to do this is to export the image to a file
docker build and save:
docker build -f Dockerfile1 -t test1 .
docker save test1 -o test1.tar
Now we will optimize this image by removing man pages (you can still find man pages on the web) and removing apt cache
3.1.2. Dockerfile optimized
FROM ubuntu:20.04 as stage1
ARG DEBIAN_FRONTEND=noninteractive
COPY 01-noDoc /etc/dpkg/dpkg.cfg.d/
COPY 02-aptNoCache /etc/apt/apt.conf.d/
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
RUN \
# remove apt cache and man/doc
rm -rf /var/cache/apt/archives /usr/share/{doc,man,locale}/ ;\
\
apt-get update ;\
apt-get install -y -q --no-install-recommends \
htop \
;\
# clean apt packages
apt-get autoremove -y ;\
ls -al /var/cache/apt ;\
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
FROM stage1 as stage2
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "xtrace", "-c"]
RUN \
echo "DEBIAN_FRONTEND=${DEBIAN_FRONTEND}"
Here the content of /etc/dpkg/dpkg.cfg.d/01-noDoc, it will tell apt to not install man docs and translations
# /etc/dpkg/dpkg.cfg.d/01_nodoc
# Delete locales
path-exclude=/usr/share/locale/*
# Delete man pages
path-exclude=/usr/share/man/*
# Delete docs
path-exclude=/usr/share/doc/*
path-include=/usr/share/doc/*/copyright
Here the content of /etc/apt/apt.conf.d/02-aptNoCache, it will instruct apt to not store any cache (note that apt-get
clean will not work after that change but you don’t need to use it anymore)
Dir::Cache "";
Dir::Cache::archives "";
Now let’s build and check the image size, the best way to do this is to export the image to a file
docker build and save:
docker build -f Dockerfile2 -t test2 .
docker save test2 -o test2.tar
Here the size of the files
test1.tar 117 020 672 bytes
test2.tar 76 560 896 bytes
We passed from ~117MB to ~76MB so we gain ~41MB Please note also that we used --no-install-recommends option in both
example that allows us to save some other MB
4 - How to Write Docker Compose Files
Guide to writing and organizing Docker Compose files
as not everyone is using the same environment (some are using MacOS for example which is targeting arm64 instead of
amd64), it is advised to add this option to target the right architecture
docker-compose platform:
services:
serviceName:
platform: linux/x86_64
# ...
2. Wait for a service to be healthy before starting another one
If you have a service that depends on another one, it is important to wait for the dependent service to be healthy
before starting the dependent one. This can be achieved using the depends_on option with the
condition: service_healthy condition.
Here is an example where serviceB depends on serviceA being healthy before it starts:
services:
serviceA:
# ...
healthcheck:
test: [CMD, curl, -f, http://localhost:8080/health]
interval: 30s
timeout: 10s
retries: 3
serviceB:
# ...
depends_on:
serviceA:
condition: service_healthy
In this example, api service will wait for the db service to be healthy before it starts. The health check for the
db service is defined to check if the MySQL server is responding to ping requests.
version: '2.1'
services:
api:
build: .
container_name: api
ports:
- 8080:8080
depends_on:
db:
condition: service_healthy
db:
container_name: db
image: mysql
ports:
- '3306'
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_DATABASE: database
healthcheck:
test: [CMD, mysqladmin, ping, -h, localhost]
timeout: 20s
retries: 10
5 - Saml2Aws Setup
Guide to setting up and using Saml2Aws for AWS access
Configure saml2aws accounts
saml2aws configure \
--idp-account='<account_alias>' \
--idp-provider='AzureAD' \
--mfa='Auto' \
--profile='<profile>' \
--url='https://account.activedirectory.windowsazure.com' \
--username='<username>@microsoft.com' \
--app-id='<app_id>' \
--skip-prompt
<app_id> is a unique identifier for the application we want credentials for (in this case an AWS environment).<account_alias> serves as a name to identify the saml2aws configuration (see your ~/.saml2aws file<profile> serves as the name of the aws cli profile that will be created when you log in.
This will automatically identify your tenant ID based on the AppID and will create a configuration based on the provided
information. Configuration will be created in ~/.saml2aws
Run saml2aws login to add or refresh your profile for the aws cli.
saml2aws login -a ${account_alias}
Follow the prompts to enter your SSO credentials and complete the multi-factor authentication step.
Note: if you are part of multiple roles you can use –role flag to configure the required role.
Above steps have been taken from below GitHub Repo. They have been tried in MacOS, Windows, Linux and Windows WSL
https://github.com/Versent/saml2aws
2. Kubernetes connection
Adding a newly created Technology Convergence EKS cluster to your ~/.kube/config:
Add EKS Cluster to ~/.kube/config
aws eks update-kubeconfig --name $clusterName --region us-east-1
3. Common issues
This is very likely because you changed your account password. Reenter your password when prompted at saml2aws login
3.2. Error - error authenticating to IdP: unable to locate SAMLRequest URL
This is very likely because you do not have access to this AWS account.
Multifactor authentication asks for a number, but the terminal doesn’t provide a number.
Solution 1: We’ve found that going to your
Microsoft account security info and deleting and re-adding the sign-in
method seems to fix the issue. You should then be able to just enter a Time-based one-time password from your Microsoft
Authenticator app.
Solution 2: You can change the MFA option for your saml2aws config either with PhoneAppOTP, PhoneAppNotification, or
OneWaySMS. Something like this in your ~/.saml2aws file
name = tc-dev
app_id = 83cffb56-1d1b-400c-ad47-345c58e378dc
url = https://account.activedirectory.windowsazure.com
username = <>@microsoft.com
provider = AzureAD
mfa = OneWaySMS
skip_verify = false
timeout = 0
aws_urn = urn:amazon:webservices
aws_session_duration = 3600
aws_profile = dev
resource_id =
subdomain =
role_arn =
region =
http_attempts_count =
http_retry_delay =
credentials_file =
saml_cache = false
saml_cache_file =
target_url =
disable_remember_device = false
disable_sessions = false
prompter =
for more reference, follow this page
https://github.com/Versent/saml2aws/blob/master/doc/provider/aad/README.md#configure
6 - Debug Hugo
Debug Hugo: Quick guide to locate Hugo templates, use templateMetrics, override priorities, and fix common Docsy/Hugo template issues for faster debugging.
1. Finding Which Template is Being Used
Method 1: Template Metrics (Recommended)
hugo server -D --templateMetrics --templateMetricsHints
This shows which templates are executed and execution times. Look for the page you’re debugging in the output.
Method 2: Add Debug Comments to Templates Add this at the top of any template to verify it’s being used:
<!-- DEBUG: Using template layouts/docs/list.html -->
{{ warnf "TEMPLATE DEBUG: Rendering %s with %s" .RelPermalink .Layout }}
Then check the HTML source or terminal output.
Method 3: Template Path in HTML (Temporary) Add to your template for debugging:
<!-- Template: {{ .Layout }} | Kind: {{ .Kind }} | Type: {{ .Type }} -->
{{ printf "
<!-- File: %s -->
" .File.Path }}
Remove after debugging to keep HTML clean.
2. Hugo Template Lookup Order
For _index.md files (list pages):
content/docs/bash-scripts/_index.md (with type: docs)
↓
1. layouts/docs/list.html ← Create this for docs sections with comments
2. layouts/docs/section.html
3. layouts/_default/list.html
4. layouts/_default/section.html
5. themes/docsy/layouts/docs/list.html
6. themes/docsy/layouts/_default/list.html
For regular .md files (single pages):
content/docs/bash-scripts/page.md (with type: docs)
↓
1. layouts/docs/single.html ← Docsy uses baseof.html with blocks
2. layouts/_default/single.html
3. layouts/partials/_td-content.html ← This is where content is rendered in Docsy
4. themes/docsy/layouts/docs/baseof.html
For blog posts:
content/blog/post.md
↓
1. layouts/blog/single.html
2. layouts/_default/single.html
3. layouts/blog/_td-content.html ← Override this for blog-specific changes
3. Common Template Debugging Commands
# Verify template exists in lookup path
find . -name "list.html" -o -name "single.html"
# Check if shared layouts are mounted correctly
hugo mod graph
# List all available templates (with jq installed)
hugo config --format json | jq '.module.mounts'
# Rebuild with verbose output
hugo server -D --logLevel debug --disableFastRender
4. Template Override Priority
- Local
layouts/ directory (highest priority) - repo-specific overrides - Mounted
shared/layouts/ from my-documents via Hugo modules - Docsy theme
themes/docsy/layouts/ (lowest priority)
| Template | Purpose | Used For |
|---|
shared/layouts/docs/list.html | Docs section index pages | _index.md with type: docs |
shared/layouts/blog/_td-content.html | Blog post content wrapper | Blog posts |
shared/layouts/_td-content.html | Regular page content wrapper | Regular docs pages |
shared/layouts/partials/giscus-comments.html | Giscus comment widget | Included in above templates |
6. Common Issues and Solutions
Issue: Comments not showing on _index.md pages
Solution: Create layouts/docs/list.html (not section.html - wrong name!)
Issue: Changes to shared/layouts/ not appearing
Solution: Run hugo mod clean && hugo mod get -u to refresh modules
Issue: Template works locally but not in CI
Solution: Check Hugo modules are committed in go.mod and go.sum
Issue: Wrong template being used
Solution: Check frontmatter type: field - it controls template lookup path
Issue: Print out the full value of a variable in Hugo Solution:
{{ printf "%#v" $pages }}<pre>{{ debug.Dump .Params }}</pre>- Use the
templates.Current function to visually mark template
execution boundaries or to display the template call stack.
7. Understanding Docsy’s Template Structure
Docsy uses a block-based template system:
baseof.html defines the overall page structure{{ block "main" }} is where content goes_td-content.html partial is called by most layouts- Override
_td-content.html to customize content rendering globally
8. Quick Debug Workflow
- Identify the page type: Regular page, section index, blog post?
- Check frontmatter: Look for
type: field (e.g., type: docs) - Find template: Use template lookup order above
- Verify template exists: Check
shared/layouts/[type]/[kind].html - Add debug output: Temporarily add
{{ warnf }} to verify - Test locally:
hugo server -D --templateMetrics --disableFastRender - Remove debug code: Clean up before committing