Jenkins CI/CD Pipeline Tutorial: From Freestyle Jobs to Jenkinsfile
Most teams don't ship code by manually running build scripts and pushing artifacts by hand — a CI/CD tool watches the repository and does it automatically, the same way, every time. Jenkins is the most widely deployed tool for this, and understanding it well is what makes the rest of a CI/CD pipeline (Docker builds, Kubernetes deploys) actually automatic instead of manually triggered.
What CI/CD actually means
Continuous Integration (CI) — every time code is pushed, it gets automatically built and tested, so integration problems surface within minutes instead of being discovered days later when someone else's changes collide with yours.
Continuous Delivery/Deployment (CD) — once a build passes, it's automatically packaged and (for Delivery) staged for a human to approve, or (for Deployment) shipped straight to production without manual intervention. The distinction matters: Delivery keeps a manual gate before production, Deployment removes it entirely.
Freestyle jobs vs. Pipeline jobs
Jenkins gives you two fundamentally different ways to define a job:
- Freestyle jobs — configured through Jenkins' web UI, point-and-click. Fine for a single simple task, but the configuration lives only in Jenkins itself — not in your repository, not version-controlled, not reviewable in a pull request.
- Pipeline jobs — defined as code in a
Jenkinsfilethat lives in your repository alongside the application it builds. This is what real production setups use almost universally now, because the pipeline definition gets the same code review, versioning and rollback safety as everything else.
Your first Jenkinsfile
A minimal declarative pipeline for a Node.js app:
pipeline {
agent any
stages {
stage('Checkout') {
steps {
git branch: 'main', url: 'https://github.com/your-org/your-repo.git'
}
}
stage('Install & Test') {
steps {
sh 'npm ci'
sh 'npm test'
}
}
stage('Build Docker Image') {
steps {
sh 'docker build -t myapp:${BUILD_NUMBER} .'
}
}
stage('Deploy') {
steps {
sh 'kubectl apply -f k8s/deployment.yaml'
}
}
}
post {
failure {
echo 'Pipeline failed — check the stage logs above.'
}
}
}
Each stage is a distinct step Jenkins runs in order, and each shows up as its own block in the Jenkins UI — so when something breaks, you immediately see which stage failed rather than digging through one giant log.
Triggering builds automatically
Two common ways a pipeline actually starts running:
- Poll SCM — Jenkins periodically checks the repository for new commits on a schedule (e.g. every 5 minutes). Simple to set up, but wasteful and slow — you're always at most one polling interval behind an actual push.
- Webhook — GitHub (or GitLab/Bitbucket) notifies Jenkins the instant a push happens, and the build starts immediately. This is the standard approach in any real setup; Poll SCM is mostly a fallback for environments where Jenkins isn't reachable from the outside for a webhook to hit.
Master/Agent architecture
A single Jenkins instance quickly becomes a bottleneck if every build runs on it directly. The Master/Agent model splits this up: the Jenkins master (now often called "controller") schedules jobs and serves the UI, while one or more agent nodes actually execute the build steps. This lets you scale horizontally (more agents = more parallel builds) and run builds in environments that match production more closely than the controller itself.
Common mistakes
Putting secrets directly in the Jenkinsfile. API keys, database passwords or deploy credentials hardcoded into pipeline code end up in your Git history forever, readable by anyone with repo access. Use Jenkins' built-in Credentials store and reference secrets by ID instead.
No post block for failure handling. Without one, a failed pipeline just... fails silently in the Jenkins UI, and nobody finds out unless they happen to check. A post { failure { ... } } block that sends a Slack/email notification is the difference between finding out in seconds versus hours later when someone asks why a deploy never happened.
Skipping tests to "save time." A pipeline stage that runs npm test but ignores a non-zero exit code (or worse, doesn't run tests at all) defeats the entire point of CI — you're now shipping whatever the last commit happened to contain, tested or not.
Where this fits in the bigger picture
Jenkins is step 4 in our DevOps roadmap — right after Git, and before containers. Once your builds and tests are automated here, the next natural step is packaging what gets built into a container — see Docker Tutorial for Beginners if you haven't already.
Want to learn this hands-on, live?
Join Waitlist