Jenkins Course
Jenkinsfile Introduction
You've seen a Jenkinsfile in action. Now let's go inside it — its structure, where it lives, how Jenkins finds it, and the patterns that make production Jenkinsfiles reliable and maintainable.
This lesson covers
Where the Jenkinsfile lives → How Jenkins finds it → The full anatomy of a Jenkinsfile → agent, environment, options, stages, post — all explained → A production-ready example
Lessons 11 and 12 gave you the why and the two syntax flavours. This lesson is about the what — what every directive in a real Jenkinsfile actually does, why it's there, and what a production-grade Jenkinsfile looks like before the team starts adding complexity.
Most Jenkinsfile tutorials show you a three-line example and call it done. Real Jenkinsfiles have ten to fifteen directives working together. By the end of this lesson you'll be able to read any Declarative Jenkinsfile and understand exactly what each section is doing.
Where the Jenkinsfile Lives
The Jenkinsfile lives in the root directory of your code repository — the same folder as your README.md, your package.json or build.gradle, your .gitignore. It sits alongside your application code, not inside Jenkins.
Typical repository structure
📁 checkout-service/
📄 Jenkinsfile ← lives here, root of the repo
📄 README.md
📄 build.gradle
📄 .gitignore
📁 src/
📁 tests/
📁 docker/
Three things to know about the Jenkinsfile name and location:
The name is always exactly Jenkinsfile — capital J, no file extension. Not jenkinsfile, not Jenkinsfile.groovy, not pipeline.jenkins. Just Jenkinsfile. Jenkins looks for this exact name by default.
You can change the path in the job config — if your team stores Jenkinsfiles in a ci/ folder, go to the Pipeline job → Configure → Pipeline → Script Path and change it to ci/Jenkinsfile. Jenkins will find it there.
Every branch can have its own Jenkinsfile — in a Multibranch Pipeline, Jenkins reads the Jenkinsfile from whichever branch it's building. Your main Jenkinsfile can deploy to production. Your feature/* Jenkinsfile can skip the deploy stage entirely.
The Full Anatomy of a Declarative Jenkinsfile
Here are all the top-level directives available in a Declarative Jenkinsfile. You don't use all of them in every pipeline — but you need to know what each one does so you can reach for the right one when you need it.
agent
Tells Jenkins where to run the pipeline — on any available agent, a specific label, a Docker container, or a Kubernetes pod. Every Declarative pipeline must have an agent directive.
environment
Defines environment variables available to all stages. Set here once, used anywhere in the pipeline. Credentials can also be injected here using the credentials() helper.
options
Pipeline-level settings — build timeout, retry count, how many old builds to keep, whether to skip the default checkout. Configured once, applies to the whole pipeline.
parameters
Defines inputs a human (or another system) can pass to the pipeline at trigger time — strings, booleans, choice dropdowns. Covered in depth in Lesson 17.
triggers
Defines what automatically fires the pipeline — a cron schedule, SCM polling, or a webhook from GitHub. Without triggers, the pipeline only runs when manually started.
stages
The container that holds all your stage blocks. Every Declarative pipeline must have exactly one stages block. All the work happens inside here.
post
Runs after all stages — notifications, cleanup, artifact publishing. Use always, success, failure, unstable, or aborted conditions to control exactly when each action fires.
tools
Automatically installs and makes available a specific version of a build tool — Maven, Gradle, JDK, Node.js. Jenkins installs the tool on the agent before stages run.
A Production-Ready Jenkinsfile
The scenario:
You're the lead DevOps engineer at a fintech company. The payments team has been using a minimal Jenkinsfile with just a Test stage. They've now asked you to upgrade it to a production-grade pipeline that includes build timeout protection, environment variables, a proper multi-stage flow, and post-build notifications. This is what you'd write.
New terms in this code:
- options { timeout() } — sets a maximum time the entire pipeline is allowed to run. If it exceeds this, Jenkins aborts it. Prevents runaway builds from blocking agents for hours.
- options { buildDiscarder() } — tells Jenkins to keep only the last N builds. Same as the "Discard old builds" setting from Lesson 10, but defined in code instead of the UI.
- environment { } — defines pipeline-wide environment variables. Every stage and step can read these variables automatically.
- credentials('id') — fetches a stored Jenkins credential by its ID and injects it as an environment variable. The value is masked in all console output — nobody can read it from the logs.
- when { branch 'main' } — a conditional directive that makes a stage only run when a specific condition is true. Here, the Deploy stage only runs when building the
mainbranch — all other branches skip it automatically. - currentBuild.result — a Jenkins built-in variable that holds the result of the current build so far — SUCCESS, FAILURE, UNSTABLE, or null if still running.
- currentBuild.displayName — the build number and name shown in the Jenkins UI. You can override it to add useful info like the version being built.
pipeline {
// Run on any agent with the 'linux' label — don't use 'any' in production
agent { label 'linux' }
// Pipeline-level options — apply to every stage
options {
// Abort the entire pipeline if it runs for more than 30 minutes
timeout(time: 30, unit: 'MINUTES')
// Keep only the last 20 builds to save disk space
buildDiscarder(logRotator(numToKeepStr: '20'))
// Add timestamps to every line of console output
timestamps()
}
// Pipeline-wide environment variables — available in every stage
environment {
APP_NAME = 'payments-service'
DEPLOY_ENV = 'staging'
// credentials() fetches the stored Jenkins credential with this ID
// Jenkins masks this value in all console output automatically
DOCKER_CREDS = credentials('docker-registry-credentials')
}
stages {
stage('Checkout') {
steps {
// Pull the code from the SCM configured in the job
checkout scm
// Set a human-readable build name in the Jenkins UI
script {
currentBuild.displayName = "#${BUILD_NUMBER} — ${APP_NAME}"
}
}
}
stage('Test') {
steps {
echo "Running tests for ${APP_NAME}"
sh './gradlew clean test'
}
// post inside a stage only runs when that stage completes
post {
always {
// Publish JUnit test results so Jenkins can track trends
junit 'build/test-results/**/*.xml'
}
}
}
stage('Build Docker Image') {
steps {
// DOCKER_CREDS_USR and DOCKER_CREDS_PSW are automatically
// created by Jenkins when you use credentials() for a username/password type
sh """
docker login -u ${DOCKER_CREDS_USR} -p ${DOCKER_CREDS_PSW} registry.acmecorp.com
docker build -t registry.acmecorp.com/${APP_NAME}:${BUILD_NUMBER} .
docker push registry.acmecorp.com/${APP_NAME}:${BUILD_NUMBER}
"""
}
}
stage('Deploy') {
// 'when' makes this stage conditional
// This stage ONLY runs when building the main branch
// All feature branches skip this stage automatically
when {
branch 'main'
}
steps {
echo "Deploying ${APP_NAME} build ${BUILD_NUMBER} to ${DEPLOY_ENV}"
sh "./deploy.sh ${DEPLOY_ENV} ${BUILD_NUMBER}"
}
}
}
// Post block runs after ALL stages complete
post {
success {
echo "✅ ${APP_NAME} pipeline passed — build ${BUILD_NUMBER} deployed to ${DEPLOY_ENV}"
}
failure {
echo "❌ ${APP_NAME} pipeline failed — build ${BUILD_NUMBER} did not deploy"
}
// 'always' runs regardless of result — good for cleanup
always {
// Clean the workspace after every build to save disk space
cleanWs()
}
}
}
Where to practice: Add this Jenkinsfile to the repository you created in Lesson 11. You'll need a Docker daemon running on your agent for the Docker stage — if you don't have that yet, comment out the Build Docker Image stage and run the rest. Store a test credential in Jenkins under Manage Jenkins → Credentials with the ID docker-registry-credentials to test the credentials injection. Full Jenkinsfile reference at jenkins.io — Pipeline Syntax.
Started by user admin
[Pipeline] Start of Pipeline
[Pipeline] node (agent-linux-01)
[Pipeline] { (Checkout)
09:14:22 Checking out https://github.com/acmecorp/payments-service.git
09:14:24 > git checkout main
09:14:24 [payments-service] $ currentBuild.displayName set to "#42 — payments-service"
[Pipeline] { (Test)
09:14:25 Running tests for payments-service
09:14:25 + ./gradlew clean test
09:15:18 BUILD SUCCESSFUL in 53s
09:15:18 41 tests completed, 0 failed
09:15:18 Recording test results
[Pipeline] { (Build Docker Image)
09:15:19 + docker login -u deployer -p **** registry.acmecorp.com
09:15:20 Login Succeeded
09:15:20 + docker build -t registry.acmecorp.com/payments-service:42 .
09:15:48 Successfully built a3f2b1c9d4e5
09:15:48 + docker push registry.acmecorp.com/payments-service:42
09:16:02 pushed: registry.acmecorp.com/payments-service:42
[Pipeline] { (Deploy)
09:16:03 Deploying payments-service build 42 to staging
09:16:03 + ./deploy.sh staging 42
09:16:31 Deployment complete.
[Pipeline] stage (post - success)
✅ payments-service pipeline passed — build 42 deployed to staging
[Pipeline] stage (post - always)
[Pipeline] cleanWs
Deleting project workspace... done
[Pipeline] End of Pipeline
Finished: SUCCESS
What just happened?
- Timestamps on every line — the
timestamps()option added09:14:22-style prefixes to every console line. In a long build this is invaluable — you can see exactly which step took the most time without doing mental arithmetic. #42 — payments-service— thecurrentBuild.displayNameoverride changed the build label in the Jenkins UI from a plain "#42" to something descriptive. This sounds minor but makes the build history page far more readable.-p ****— Jenkins automatically masked the Docker password in the console output. The four stars replace the actual credential value. This is thecredentials()injection working correctly — the password was used but never exposed in any log.41 tests completed, 0 failedthenRecording test results— the stage-levelpost { always { junit ... } }fired immediately after the Test stage completed, publishing the JUnit XML results to Jenkins. This lets Jenkins track test trends across builds and highlight flaky tests.- Deploy stage ran — because this was a build of the
mainbranch, thewhen { branch 'main' }condition evaluated true and the stage executed. On a feature branch, this stage would be skipped entirely and you'd see[Pipeline] { (Deploy) skipped }in the output. cleanWs()— the workspace was deleted after the build. The agent's disk won't accumulate leftover build files. This is thealwayspost condition — it fired even though the build succeeded.
Patterns You'll See in Every Real Jenkinsfile
Always clean the workspace
Put cleanWs() in a post { always { } } block. Without this, every build leaves files on the agent. Over time the disk fills up and builds start failing with "No space left on device" errors at 2 AM.
Use environment variables for anything that changes
If you hardcode staging in three different shell commands, you'll miss one when you need to change it. Define it once in the environment { } block and reference it everywhere with ${DEPLOY_ENV}.
Set a timeout — always
Without a timeout, a hanging test or a network hiccup can block an agent executor for days. A 30-minute timeout on most pipelines means you find out about hangs before your next standup, not the next morning.
Publish test results in the stage-level post
Use post { always { junit '...' } } inside the Test stage rather than in the global post block. That way Jenkins publishes results even if the test stage fails — which is exactly when you need the test report most.
Gate deploys with when { branch 'main' }
Never deploy from a feature branch to production. The when directive is the clean way to enforce this without messy if/else logic scattered across stages.
Teacher's Note
Copy the production Jenkinsfile above into every new project as your starting point. Delete the stages you don't need yet. Add back when you do. It's easier to remove than to remember to add.
Practice Questions
1. Which Jenkinsfile directive makes a stage conditionally skip itself based on the branch name or other criteria?
2. What Jenkins Pipeline step removes all files from the agent's workspace after a build completes?
3. What helper function do you use inside the environment { } block to inject a stored Jenkins credential as an environment variable?
Quiz
1. What does timeout(time: 30, unit: 'MINUTES') in the options block do?
2. Where should the Jenkinsfile be placed in your code repository?
3. When you inject a credential using credentials() in the environment block, what does Jenkins do to protect it?
Up Next · Lesson 14
Pipeline Syntax
Every directive, every block, every option in Declarative syntax — the complete reference with real examples for each one.