Start hereQuick start · 6 you’ll reach for daily
Target versions · paceVersions
jenkins LTS ≥ 2.452
Java 17 / 21
Pipeline plugin (declarative)
JCasC (Configuration as Code)
Always run the LTS line in production — weekly releases ship breakage. Java 17 is the
floor since LTS 2.426; new LTS lines require Java 21. Declarative pipelines (the
pipeline { ... } shape) are the strong default;
scripted (node('...') { ... }) is the older Groovy DSL
— use it only when declarative isn’t expressive enough.
Docker · plugins · JCasCSetup
# 1. Run Jenkins LTS in Docker with persistent state
docker volume create jenkins_home
docker run -d --name jenkins \
-p 8080:8080 -p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins/jenkins:lts-jdk21
# 2. Initial admin password
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
# 3. Pre-install plugins from a plugins.txt (one per line: short-name[:version])
cat > plugins.txt <<'TXT'
workflow-aggregator
configuration-as-code
github-branch-source
docker-workflow
kubernetes
credentials-binding
htmlpublisher
junit
blueocean
TXT
docker exec jenkins \
jenkins-plugin-cli --plugin-file /tmp/plugins.txt \
--plugin-download-directory /var/jenkins_home/plugins
# 4. Configuration-as-Code — declarative instance config
# point Jenkins at it via env var
docker exec jenkins \
env CASC_JENKINS_CONFIG=/var/jenkins_home/casc.yaml || true
# casc.yaml: define security realm, credentials, jobs, tools — all in YAML
# 5. CLI from outside
java -jar jenkins-cli.jar -s http://localhost:8080/ -auth admin:**** list-jobs
pipeline { ... }Declarative pipeline
| pipeline { agent any; stages { ... } } | Top-level wrapper. Required. |
| agent any | Any free executor on the controller or an agent. |
| agent { label 'linux && docker' } | Match labels with boolean expressions. |
| agent { docker { image 'maven:3.9' } } | Run the whole pipeline inside a container. |
| agent { kubernetes { yaml '...' } } | Spin up a pod-per-build via the Kubernetes plugin. |
| agent none | No global agent — each stage picks its own. |
| stages { stage('X') { steps { ... } } } | Ordered execution blocks. Each must have steps. |
| steps { sh 'make build' } | Shell step. bat on Windows, powershell for PowerShell. |
| steps { echo 'msg' } | Print to console log. |
| environment { FOO = 'bar' } | Pipeline / stage-scoped env vars. |
| tools { jdk 'jdk21'; maven 'maven-3.9' } | Install tools from "Global Tool Configuration". |
| post { always { ... } success { ... } failure { ... } } | Lifecycle hooks. Run regardless of stage outcome. |
| when { branch 'main' } | Skip a stage unless the predicate matches. |
| parameters { choice(...) booleanParam(...) string(...) password(...) } | Surface UI parameters on the "Build with Parameters" page. |
| triggers { cron('H 2 * * 1-5'); githubPush() } | Schedule + SCM triggers. H spreads load across the hour. |
// Jenkinsfile — declarative pipeline at repo root.
pipeline {
agent { label 'linux && docker' }
options {
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
ansiColor('xterm')
}
parameters {
choice(name: 'TARGET_ENV', choices: ['dev', 'stg', 'prod'], description: 'deploy target')
booleanParam(name: 'RUN_E2E', defaultValue: false)
}
environment {
REGISTRY = 'ghcr.io/my-org'
APP = 'web'
}
triggers {
githubPush()
cron('H 2 * * 1-5') // weekday early-morning rebuilds
}
stages {
stage('Build') {
steps { sh 'make build' }
}
stage('Test') {
parallel {
stage('unit') { steps { sh 'pytest --junitxml=junit.xml' } }
stage('lint') { steps { sh 'ruff check .' } }
}
post { always { junit 'junit.xml' } }
}
stage('Deploy') {
when { branch 'main'; beforeAgent true }
steps {
input message: "Deploy to ${params.TARGET_ENV}?", ok: 'Go'
sh "./deploy.sh ${params.TARGET_ENV}"
}
}
}
post {
always { archiveArtifacts artifacts: 'build/*.zip', fingerprint: true }
failure { slackSend channel: '#ci-alerts', message: "Pipeline FAILED: ${env.BUILD_URL}" }
}
}
parallel · matrix · whenStages
| parallel { stage('a'){...} stage('b'){...} } | Run sub-stages concurrently. |
| parallel(failFast: true) { ... } | Cancel siblings on first failure. |
| matrix { axes { axis { name 'OS'; values 'linux','mac' } } stages {...} } | Cartesian product of axes — one job per combo. |
| matrix { excludes { exclude { axis {...} } } } | Drop specific combinations. |
| when { branch 'main' } | Branch predicate (supports wildcards). |
| when { tag pattern: 'v.*', comparator: 'REGEXP' } | Tag predicate. |
| when { changeset 'src/**' } | Run only if matching paths changed. |
| when { expression { env.FOO == 'bar' } } | Arbitrary Groovy predicate. |
| when { beforeAgent true; ... } | Evaluate the when before grabbing the agent — saves an executor slot. |
| input message: 'Deploy?', ok: 'Go', submitter: 'admins' | Manual approval step. Pauses the pipeline. |
| stash { name: 'binaries', includes: 'build/**' } | Cache file set for downstream stages / nodes. |
| unstash 'binaries' | Pull a stash into the current workspace. |
labels · containers · k8sAgents
| Controller | The Jenkins server itself. Don’t run builds on it in prod. |
| Permanent agent (SSH / JNLP) | Long-lived host. JNLP "inbound" agents avoid open SSH. |
| Docker / docker-pipeline plugin | Spin up a container per stage with agent { docker }. |
| Kubernetes plugin | One pod per build — ephemeral, autoscaled. |
| EC2 / Cloud plugins | Spin up cloud VMs on demand; tear them down after. |
| label 'linux && docker && !arm64' | Boolean expressions on labels — the standard targeting mechanism. |
| node('linux') { ... } | Scripted-style agent grab. Reserve an executor for the block. |
| customWorkspace '/build/ws' | Override the workspace path. Useful for path-length-limited Windows. |
| retainBuildExecutor false | Free the executor while paused on input. |
Built-in stepsPipeline DSL
| checkout scm | Clone the SCM that triggered the build. |
| git url: '...', branch: 'main', credentialsId: 'gh' | Explicit Git clone. |
| sh 'cmd' · bat 'cmd' · powershell 'cmd' | Shell on Linux/macOS, batch/PowerShell on Windows. |
| sh returnStdout: true, script: 'date' | Capture stdout into a Groovy variable. |
| sh returnStatus: true, script: '...' | Return exit code instead of failing on non-zero. |
| readFile 'pom.xml' · writeFile | Workspace file IO without shelling out. |
| readJSON / readYaml / readMavenPom | Structured-file parsers (Pipeline-utility-steps plugin). |
| junit 'junit.xml' | Parse JUnit XML — feeds the Tests tab + flaky-test data. |
| archiveArtifacts artifacts: 'build/*.zip', fingerprint: true | Upload build artifacts. Browseable + traceable. |
| publishHTML target: [reportDir: 'cov', reportFiles: 'index.html', ...] | Attach an HTML report (coverage / lighthouse). |
| slackSend channel: '#alerts', message: 'msg' | Slack notification (slack plugin). |
| emailext to: '...', subject: '...', body: '...' | Rich email notifications (email-ext plugin). |
| timeout(time: 30, unit: 'MINUTES') { ... } | Abort wrapped block after N units. |
| retry(3) { sh '...' } | Re-run on failure up to N times. |
| catchError(...) { ... } | Catch a failure but record it on the build. |
| error 'reason' | Fail the build with a message. |
node { ... } · full GroovyScripted pipeline
| node('label') { ... } | Acquire an executor and run the block on it. |
| stage('Name') { ... } | Visual stage on the Stage View / Blue Ocean. |
| parallel a: { ... }, b: { ... }, failFast: true | Run named branches concurrently. |
| try / catch / finally | Plain Groovy control flow — precise error handling. |
| docker.image('img').inside('-v ...') { ... } | Run nested steps inside a container with options. |
| script { ... } | Inside declarative: drop into scripted mode for one block. |
| env.BUILD_NUMBER · env.JOB_NAME · env.BRANCH_NAME | Built-in env vars (always populated). |
| currentBuild.result = 'UNSTABLE' | Mark the build manually. |
// Scripted pipeline — Groovy with full programmability.
// Use when declarative isn't expressive enough (rare).
node('linux && docker') {
try {
stage('Checkout') {
checkout scm
}
stage('Build') {
// Run inside a container without losing the workspace
docker.image('maven:3.9-eclipse-temurin-21').inside('-v $HOME/.m2:/root/.m2') {
sh 'mvn -B -DskipTests package'
}
}
// Parallel scripted stages
stage('Verify') {
parallel(
unit: { sh 'mvn test' },
integration: { sh 'mvn verify -DskipUnitTests' },
lint: { sh 'mvn spotbugs:check' },
failFast: true,
)
}
stage('Publish') {
if (env.BRANCH_NAME == 'main') {
withCredentials([usernamePassword(
credentialsId: 'ghcr',
usernameVariable: 'U', passwordVariable: 'P')]) {
sh 'echo $P | docker login ghcr.io -u $U --password-stdin'
sh 'docker push ghcr.io/my-org/app:${BUILD_NUMBER}'
}
} else {
echo "Skipping publish on branch ${env.BRANCH_NAME}"
}
}
}
catch (err) {
currentBuild.result = 'FAILURE'
throw err
}
finally {
junit testResults: '**/target/surefire-reports/*.xml', allowEmptyResults: true
}
}
Reusable Groovy codeShared libraries
| @Library('platform-shared@v1.4.0') _ | Import a library at a specific ref. The trailing _ is required. |
| @Library(['lib-a@main', 'lib-b@v2']) _ | Multiple libraries in one annotation. |
| vars/myStep.groovy | Becomes a global step myStep() callable from any pipeline. |
| def call(Map cfg) { ... } | Standard signature for the global step. |
| src/org/example/Foo.groovy | Classic Groovy classes. Reusable from vars/. |
| resources/templates/x.tpl | Static resources fetched via libraryResource('templates/x.tpl'). |
| Manage Jenkins → Global Pipeline Libraries | Configure default library / repo / version. |
| @Library('lib') import org.example.* | Pull in classes directly when using src/. |
// Shared library layout (separate Git repo):
// vars/
// deployApp.groovy // exposed as a global step
// src/
// org/example/Builder.groovy
// resources/
// templates/dockerfile.tpl // libraryResource('templates/dockerfile.tpl')
// vars/deployApp.groovy
def call(Map cfg = [:]) {
def env = cfg.get('env', 'dev')
def app = cfg.get('app', env.JOB_NAME)
def wait = cfg.get('wait', true)
stage("Deploy ${app} → ${env}") {
withCredentials([file(credentialsId: "kube-${env}", variable: 'KUBECONFIG')]) {
sh "kubectl apply -f k8s/${env}/"
if (wait) {
sh "kubectl rollout status deployment/${app} --timeout=5m"
}
}
}
}
// Jenkinsfile in the consuming repo — pin the library to a tag for reproducibility.
@Library('platform-shared@v1.4.0') _
pipeline {
agent any
stages {
stage('Build') { steps { sh 'make build' } }
stage('Deploy') {
when { branch 'main' }
steps {
deployApp(app: 'checkout', env: 'prod', wait: true)
}
}
}
}
withCredentials · maskingCredentials
| Manage Jenkins → Credentials | Central store. Scopes: system, global, per-folder, per-user. |
| usernamePassword(credentialsId, usernameVariable, passwordVariable) | Binds U + P env vars for the block. |
| string(credentialsId, variable: 'TOKEN') | Single-string secret — API tokens. |
| file(credentialsId, variable: 'KUBECONFIG') | Mount a secret file; variable holds the path. |
| sshUserPrivateKey(credentialsId, keyFileVariable, usernameVariable) | SSH key + username for ssh -i usage. |
| certificate(credentialsId, keystoreVariable, passwordVariable) | PKCS12 keystore. |
| withCredentials([...]) { ... } | Wrap the block needing the secrets. Vars are unset after. |
| credentials('id') | Short-hand inside environment { FOO = credentials('id') } — expands to FOO_USR / FOO_PSW. |
| Mask Passwords plugin | Secrets are masked in console logs automatically — never echo them. |
| Vault / AWS Secrets Manager plugins | Resolve credentials at runtime from an external store. |
// Credentials are stored centrally and bound to env vars at step time.
// Manage Jenkins → Credentials → System → Global credentials.
pipeline {
agent any
stages {
stage('Docker login (username+password)') {
steps {
withCredentials([usernamePassword(
credentialsId: 'docker-hub',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS')]) {
sh 'echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin'
}
}
}
stage('API token (secret text)') {
steps {
withCredentials([string(credentialsId: 'gh-token', variable: 'GH_TOKEN')]) {
sh 'curl -H "Authorization: bearer $GH_TOKEN" https://api.github.com/user'
}
}
}
stage('Kubeconfig (secret file)') {
steps {
withCredentials([file(credentialsId: 'kube-prod', variable: 'KUBECONFIG')]) {
sh 'kubectl get pods -A'
}
}
}
stage('SSH key') {
steps {
withCredentials([sshUserPrivateKey(
credentialsId: 'deploy-key',
keyFileVariable: 'KEY_FILE',
usernameVariable: 'USER')]) {
sh 'ssh -i $KEY_FILE $USER@host "uptime"'
}
}
}
}
}
// Values are masked in the console log automatically.
cron · SCM · upstreamTriggers
| triggers { cron('H 2 * * 1-5') } | Cron schedule. H = hashed minute (spreads load). |
| triggers { pollSCM('H/5 * * * *') } | Poll the SCM for changes. Avoid — prefer push-based hooks. |
| triggers { githubPush() } | GitHub webhook trigger (github plugin). |
| triggers { gitlab() } | GitLab webhook trigger. |
| triggers { upstream('parent-job', threshold: SUCCESS) } | Trigger from another job’s result. |
| Multibranch pipeline + Branch Sources | Auto-discovers branches + MRs. Each branch gets its own pipeline. |
| parameters: build job: 'down', parameters: [string(...)] | Trigger downstream from a pipeline step. |
UX layersUI
| Blue Ocean | Modern UI for pipelines — stage graph, log drill-in. Plugin-based. |
| Stage View (Pipeline plugin) | Stripped-down stage timeline in the classic UI. |
| Build with Parameters | Auto-generated form from parameters {...}. |
| Replay | Re-run a build with a one-off edited pipeline — great for iteration. |
| Pipeline Syntax (Snippet Generator) | UI form → generates Groovy for any installed step. |
| Console Output | Raw log per build. /log URL is bookmarkable. |
Day-one setEssential plugins
| workflow-aggregator | "Pipeline" itself — declarative + scripted + steps. |
| credentials-binding | withCredentials step + bindings. |
| docker-workflow | docker.image('...').inside { } + agent { docker }. |
| kubernetes | Pod-per-build agents on K8s. |
| github-branch-source / gitlab-branch-source | Multibranch + PR/MR discovery. |
| junit / htmlpublisher / coverage | Test reports, HTML reports, coverage UI. |
| configuration-as-code (JCasC) | Preferred Declarative instance config — checked in YAML. |
| job-dsl | Define seed jobs as Groovy. Complements JCasC. |
| slack / email-ext | Notifications. |
| blueocean | Modern UI plugin bundle. |
| build-monitor-plugin | Big-screen dashboard for radiators. |
Configuration as CodeJCasC
| CASC_JENKINS_CONFIG=/path/to/casc.yaml | Env var pointing at the YAML (or directory). |
| jenkins: securityRealm: local: ... | Define the security realm and user store. |
| jenkins: authorizationStrategy: globalMatrix: ... | Permissions model in YAML. |
| credentials: system: domainCredentials: [...] | Bootstrap secrets from the env / external secret stores. |
| unclassified: globalLibraries: libraries: [...] | Wire in shared libraries declaratively. |
| tool: jdk: installations: [...] maven: installations: [...] | Global Tool Configuration as code. |
| Manage Jenkins → Configuration as Code → Reload existing configuration | Apply changes without restart. |
| jenkins.yaml + job-dsl → seed jobs | Standard combo: JCasC for the instance, job-dsl for the catalogue of jobs. |
Full pipeline · ~60 linesEnd-to-end · Matrix → build → deploy
Matrix test across Node versions and DB backends, image build with GHCR credentials, manual approval gate, and a Kubernetes deploy via a shared-library global step.
// End-to-end Jenkinsfile: matrix tests → image build → manual approval → k8s deploy.
@Library('platform-shared@v1.4.0') _
pipeline {
agent none
options {
timeout(time: 45, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '30'))
disableConcurrentBuilds()
ansiColor('xterm')
}
parameters {
choice(name: 'ENV', choices: ['stg', 'prod'])
}
stages {
stage('Test matrix') {
matrix {
agent { label 'linux && docker' }
axes {
axis { name 'NODE'; values '20', '22' }
axis { name 'BACKEND'; values 'postgres', 'mysql' }
}
stages {
stage('test') {
steps {
sh "docker compose -f compose.${BACKEND}.yml up -d"
sh "nvm exec ${NODE} npm test -- --ci --reporters=jest-junit"
}
post {
always { junit 'junit.xml' }
cleanup { sh 'docker compose down -v' }
}
}
}
}
}
stage('Build image') {
agent { label 'linux && docker' }
steps {
withCredentials([usernamePassword(credentialsId: 'ghcr',
usernameVariable: 'U',
passwordVariable: 'P')]) {
sh 'echo $P | docker login ghcr.io -u $U --password-stdin'
sh "docker build -t ghcr.io/my-org/app:${BUILD_NUMBER} ."
sh "docker push ghcr.io/my-org/app:${BUILD_NUMBER}"
}
}
}
stage('Deploy') {
when { branch 'main' }
steps {
input message: "Deploy to ${params.ENV}?", ok: 'Go'
deployApp(app: 'web', env: params.ENV)
}
}
}
post {
failure { slackSend channel: '#ci-alerts', message: "FAILED: ${env.BUILD_URL}" }
success { slackSend channel: '#deploys', message: "✓ ${env.JOB_NAME} #${env.BUILD_NUMBER}" }
}
}
Best practiceGood to know
script { ... } only for genuinely dynamic logic — otherwise the next person
maintaining the pipeline pays the price.
main.
@Library('platform-shared@v1.4.0') means a library change can’t silently
break every pipeline at 3am. Bump deliberately, ride one repo at a time.
Common trapsWatch out for
sh 'rm -rf $WORKSPACE/..' can wipe configs. Set controller executors to 0 and use agents.