After containerizing applications with Docker (Project 3) and understanding the manual CI/CD workflow, the next step was obvious: automate it. That's where Jenkins comes in.
But here's what I didn't expect: automating one pipeline is straightforward. Automating ten nearly identical pipelines? That's where things get messy fast. And that's exactly the problem Jenkins Shared Libraries solve.
This project took me from writing basic Jenkinsfiles to building reusable pipeline code that can be shared across multiple projects. I learned why enterprises don't just copy-paste pipeline logic, how Groovy classes work differently inside Jenkins, and why the script object is simultaneously confusing and essential.
It took about 8 hours over two days. And I finally understand how companies manage hundreds of CI/CD pipelines without losing their minds.
The Problem (Or: Why Copy-Paste Doesn't Scale)
The scenario:
Imagine you're working on a microservices application with 10 different services:
- user-authentication
- payment-processing
- order-management
- inventory-service
- notification-service
- ...and 5 more
All of them are Java Maven applications. All of them need Jenkins pipelines. And 90% of the pipeline logic is identical:
- Build JAR with Maven
- Build Docker image
- Login to Docker registry
- Push image to registry
Without Jenkins Shared Library, you'd have:
- 10 repositories
- 10 Jenkinsfiles
- 10 copies of the same build/push logic
- 100+ lines of duplicated code per repo
What happens when something changes?
- Nexus repository URL changes? Update 10 files.
- Switch from Maven to Gradle? Update 10 files.
- New Docker registry? Update 10 files.
- Bug in build logic? Fix it 10 times.
This is a maintenance nightmare. And it gets worse with every new microservice you add.
The Solution: Extract the Logic
Jenkins Shared Library is a separate Git repository containing reusable Groovy code. Instead of copying the same pipeline logic into every project, you write it once and reference it from all your Jenkinsfiles.
The concept:
jenkins-shared-library (Git repo)
├── buildJar()
├── buildDockerImage()
├── dockerLogin()
└── dockerPush()
microservice-1 → calls buildJar(), buildDockerImage()...
microservice-2 → calls buildJar(), buildDockerImage()...
microservice-3 → calls buildJar(), buildDockerImage()...
One source of truth. Ten projects using it. Change once, affect everywhere.
Part 0: The Jenkins Journey So Far
Before diving into Shared Libraries, let me back up and explain how I got here.
Jenkins Setup
I'm running Jenkins on a DigitalOcean Ubuntu 22.04 droplet as a Docker container. The setup process was:
- Create a $48/month droplet (4GB RAM, 4 vCPUs, 160GB storage)
- Install Docker on Ubuntu
- Run Jenkins:
docker run -d -p 8080:8080 -p 50000:50000 -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts - Initialize Jenkins and install plugins (Pipeline, Git, Docker)
- Configure build tools (Maven 3.9)
The tricky part: Making Docker available inside Jenkins.
Since Jenkins is running in a Docker container, and I need Jenkins to build Docker images, I had to mount the Docker socket from the host:
docker run -d -p 8080:8080 -p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins/jenkins:lts
This lets Jenkins use the host's Docker daemon (Docker-in-Docker without actually nesting containers).
But there's a catch: Mounting the socket alone doesn't give you the docker command inside the Jenkins container. You need to install Docker CLI.
I had to access the running Jenkins container and install Docker:
# Get the container ID
docker ps
# Access the container as root
docker exec -u root -it <container-id> bash
# Inside the container:
apt-get update
apt-get install -y docker.io
# Verify
docker --version
Now Jenkins can execute docker build, docker push, etc. in pipeline steps.
Jenkins Plugin and Tool Configuration
Plugins configured:
- Pipeline — For Jenkinsfile support
- Git — For repository integration
- Maven Integration — For Maven builds
- NodeJS — For Node.js projects (installed for future use)
Then I configured build tools in Manage Jenkins → Tools:
Maven configuration:
- Name:
maven-3.9 - Install automatically: ✅
- Version: 3.9.12
This is why my Jenkinsfile can reference:
tools {
maven 'maven-3.9'
}
Git configuration:
- Git installations: Default (uses system git)
Credentials setup — GitLab:
- Jenkins → Manage Jenkins → Credentials → System → Global credentials → Add Credentials
- Type: Username with password
- Username: my GitLab username
- Password: my GitLab password
- ID: (auto-generated UUID like
b12881fa-58d1-4a47-828f-970217c47e67) - Description: GitLab credentials
Credentials setup — Docker Hub:
- Type: Username with password (not personal access token, not SSH key)
- Username: my Docker Hub username
- Password: my Docker Hub password
- ID:
0849fe1b-1263-4101-9fcf-1a7bb0b567d4 - Description: Docker Hub credentials
Important: Later when I reference these in Groovy code, I use the UUID (the ID field), not the description or username.
The Progression I Actually Took
I didn't start with Shared Libraries. I went through multiple stages of learning Jenkins:
Stage 1: Freestyle Jobs
I started by clicking through the Jenkins UI. Everything was manual: New Item → Freestyle project → configure Git → add shell build steps. This is how I got the first builds working, but it was all UI clicks, no version control.
My actual Freestyle job build steps:
chmod +x freestyle-build.sh
./freestyle-build.sh
The shell script contained the Docker build and push logic. Later, this exact logic would move into the shared library as buildImage() and dockerPush() functions.
Build step 2 was "Invoke top-level Maven targets" with goals: clean package. Later, I simplified to just mvn package—the clean wasn't necessary since Jenkins builds in fresh workspaces.
Stage 2: Pipeline Jobs with Jenkinsfile
Then I learned about Pipeline jobs. Now my pipeline was code:
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn package'
sh 'docker build -t my-app .'
}
}
}
}
Better! The pipeline is version controlled. But everything was in one massive Jenkinsfile.
Stage 3: External Groovy Scripts
Next, I learned to extract logic into external Groovy scripts. I created script.groovy:
def buildJar() {
echo "Building JAR..."
sh 'mvn package'
}
def deployApp() {
echo "Deploying application..."
// Environment-specific deployment logic
}
return this
And loaded it in my Jenkinsfile:
def gv
pipeline {
stages {
stage("init") {
steps {
script {
gv = load "script.groovy"
}
}
}
stage("build") {
steps {
script {
gv.buildJar()
}
}
}
stage("deploy") {
steps {
script {
gv.deployApp()
}
}
}
}
}
This is cleaner. But script.groovy lives in the project repository. Every project has its own copy. If 10 projects all need buildJar(), I'm still copying the same logic 10 times.
Stage 4: Multibranch Pipeline
Regular Pipeline job: Points to one Git branch. If you have feature branches, you need to create separate jobs manually for each one.
Multibranch Pipeline: Automatically discovers all branches in your repository and creates pipeline jobs for each one. Push a new branch? Jenkins creates a job. Delete a branch? Jenkins removes the job.
How I configured it:
- Jenkins → New Item → Multibranch Pipeline
- Branch Sources → Git → Repository URL and credentials
- Scan Multibranch Pipeline Triggers → Periodically if not otherwise run (every hour)
Key benefit: The BRANCH_NAME environment variable is automatically available in multibranch pipelines. This is why my buildJar() function can echo "building the application for branch $BRANCH_NAME"—it works for master, develop, feature branches, all automatically.
Stage 5: Shared Library (This Project)
This is where I finally extracted the common logic into a separate repository that all projects can reference.
Why this progression matters: My current Jenkinsfile still loads script.groovy for deployApp(). You might wonder why, if I have a shared library. Answer: deployment logic is environment-specific. Build logic (Maven, Docker) is universal. Universal logic moves to the shared library. Environment-specific logic stays in the project.
Timeline: This entire progression (Freestyle → Pipeline → Scripts → Multibranch → Shared Library) took about a week of learning. The shared library project itself took 4 hours in one afternoon — but only because I'd already built the foundational knowledge.
Early Problems: The Maven Version Issue
Before I could even build pipelines successfully, I had to fix the application itself. The Java Maven app had a pom.xml with this:
<version>3.5.5.RELEASE</version>
Could not find artifact org.springframework.boot:spring-boot-maven-plugin:jar:3.5.5.RELEASE
The problem: Spring Boot 3.5.5 exists, but the version string 3.5.5.RELEASE doesn't. Spring Boot 2.x used the .RELEASE suffix, but Spring Boot 3.x dropped it. Someone mixed the old naming convention with the newer version number.
The fix: Changed both occurrences to 3.5.5. This was the second time I hit version compatibility issues (first was Gradle in Project 1), reinforcing that dependency management is a constant concern in Java projects.
Part 1: Early Pipeline Syntax Errors
During my initial learning of Pipeline jobs, I hit several Groovy syntax errors that taught me the language's quirks.
The First Error: Missing Colon
I was trying to create an input prompt to select deployment environment:
env.ENV = input message: "Select environment", ok "Done", parameters: [choice(name: 'ONE', choices: ['dev', 'staging', 'prod'])]
expecting '}', found ',' @ line 44, column 94.
nment to deploy to", ok "Done", paramete
^
The problem: ok "Done" should be ok: "Done". Groovy named parameters need colons.
env.ENV = input message: "Select environment", ok: "Done", parameters: [...]
The Second Error: Variable Typo
After fixing the syntax, I got a runtime error:
No such property: ECHO for class: groovy.lang.Binding
I had defined ENV, then tried to use ECHO. Classic typo. Fixed it to ${ENV}.
How to Read Jenkins Errors
Jenkins stack traces are intimidating. Here's the pattern:
- First line after "exception:" — The actual error
- WorkflowScript.run(line X) — Which line in your Jenkinsfile
- Everything else — Jenkins internal code (usually ignore)
No such property: ECHO for class: groovy.lang.Binding
WorkflowScript.run(WorkflowScript:47)
Translation: "Line 47 is trying to use a variable called ECHO, but it doesn't exist." Once I understood this pattern, debugging got much faster.
Part 2: Creating the Shared Library Repository
With Jenkins working and pipelines building successfully, I started building the shared library. First step: create the repository structure.
The Required Structure
Jenkins Shared Library has a specific directory structure:
jenkins-shared-library/
├── vars/ # Global functions (the main folder)
│ ├── buildJar.groovy
│ ├── buildImage.groovy
│ ├── dockerLogin.groovy
│ └── dockerPush.groovy
├── src/ # Helper classes
│ └── com/example/
│ └── Docker.groovy
└── resources/ # Non-Groovy files (SQL, shell scripts, JSON)
The vars/ folder: Contains functions you call directly from Jenkinsfiles. Each .groovy file becomes a global function — file name = function name.
The src/ folder: Contains helper classes for complex logic. Standard Groovy class structure, imported and used by vars/ functions.
The resources/ folder: External libraries and non-Groovy files. I didn't use this for this project.
Creating the Repository
I created the shared library repository using this workflow:
1. Create the structure locally in IntelliJ:
jenkins-shared-library/
├── vars/
│ ├── buildJar.groovy
│ ├── buildImage.groovy
│ └── (other functions)
└── src/
└── com/example/
└── Docker.groovy
2. Create the repository on GitLab:
- GitLab UI → New Project →
jenkins-shared-library - Initialize with README: No (I'm creating locally first)
3. Initialize Git locally and connect to the remote:
cd jenkins-shared-library
git init
git add .
git commit -m "Initial commit"
git remote add origin https://gitlab.com/nvastola/jenkins-shared-library.git
git push -u origin master
Now the shared library is in version control and accessible to Jenkins.
Part 3: Writing the First Function (buildJar)
I started by extracting the Maven build logic into a shared function.
Original Jenkins pipeline step:
stage("build jar") {
steps {
script {
echo "Building the application..."
sh 'mvn package'
}
}
}
Extracted into vars/buildJar.groovy:
#!/user/bin/env groovy
def call() {
echo "building the application for branch $BRANCH_NAME"
sh 'mvn package'
}
Key concepts:
- The shebang line:
#!/user/bin/env groovytells your editor this is a Groovy script - The
call()method: Required. This is what gets executed when you callbuildJar()from a Jenkinsfile - Direct access to Jenkins commands: Inside
vars/functions, you can useecho,sh,withCredentials, etc. directly - The
$BRANCH_NAMEvariable: A Jenkins environment variable automatically available in multibranch pipelines
Updated Jenkinsfile:
@Library('jenkins-shared-library') _
pipeline {
agent any
stages {
stage("build jar") {
steps {
script {
buildJar() // That's it!
}
}
}
}
}
From ~5 lines of logic to 1 function call. And now every project can use buildJar() without rewriting it.
Groovy lets you call functions with or without parentheses. You'll see both buildJar() and buildImage 'imageName' in Jenkins pipelines — both work the same way.
Part 4: The Docker Image Function (First Attempt)
Next, I extracted the Docker build logic. This is where things got complicated.
Original pipeline logic:
stage("build image") {
steps {
script {
echo "Building the Docker image..."
withCredentials([usernamePassword(credentialsId: 'nexus-credentials', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
sh "docker build -t my-app:${BUILD_NUMBER} ."
sh "echo $PASS | docker login -u $USER --password-stdin nexus.example.com"
sh "docker push my-app:${BUILD_NUMBER}"
}
}
}
}
This worked! But the function did three different things. What if another project needs to login to Docker but not build? What if I want to build but push to a different registry?
Better approach: Split into three separate functions:
buildDockerImage(imageName)— Just builddockerLogin()— Just authenticatedockerPush(imageName)— Just push
This is where I learned about the Single Responsibility Principle in pipeline code. Each function should do one thing.
Part 5: Using Classes (Understanding the script Object)
For better organization, I wanted to move the Docker logic into a proper Groovy class. This is where I learned an important concept about how Jenkins Shared Libraries work.
Inside vars/ functions, Jenkins pipeline commands (echo, sh, withCredentials) are available globally. But inside classes in src/, they're not available by default. You have to explicitly pass the Jenkins context through a special object called script.
Creating the Docker Class
Updated Docker class with the script pattern:
#!/user/bin/env groovy
package com.example
class Docker implements Serializable {
def script // This holds the Jenkins context
Docker(script) {
this.script = script
}
def buildDockerImage(String imageName) {
script.echo 'building the docker image...'
script.sh "docker build -t $imageName ."
}
def dockerLogin() {
script.withCredentials([script.usernamePassword(credentialsId: '0849fe1b-1263-4101-9fcf-1a7bb0b567d4', passwordVariable: 'PASS', usernameVariable: 'USER')]) {
script.sh "echo '${script.PASS} | docker login -u '${script.USER} --password-stdin"
}
}
def dockerPush(String imageName) {
script.sh "docker push $imageName"
}
}
Key changes:
implements Serializable— Required for Jenkins to save the state between pipeline steps- Constructor takes
script— We pass the Jenkins context when creating the object - All commands use
script.—script.echo,script.sh,script.withCredentials - Variables use single quotes — Prevents interpolation issues with special characters in passwords
Why the Credential Syntax Is Weird
Look at this line:
script.sh "echo '${script.PASS} | docker login -u '${script.USER} --password-stdin"
Why the single quotes around the variables?
I learned this the hard way. My first attempt used double quotes around the password variable. The credentials were correct, but Docker kept returning "unauthorized: incorrect username or password."
The problem: My password contained special characters ($, !, etc.) that bash was interpreting as variables or commands. Without the single quotes, the shell tried to expand $ symbols in the password.
The fix: Wrap variables in single quotes to pass them literally to the shell. Now bash treats the password as a literal string, special characters and all.
Using the Class from vars/
Now I could use this class from vars/buildImage.groovy:
#!/usr/bin/env groovy
import com.example.Docker
def call(String imageName) {
return new Docker(this).buildDockerImage(imageName)
}
What's happening:
- Import the Docker class
- Create a new instance:
new Docker(this) - The
thiskeyword passes the current Jenkins context (the script object) - Call the method:
.buildDockerImage(imageName)
Part 6: Splitting Into Multiple Functions
With the class working, I split the Docker functionality into separate functions:
vars/buildImage.groovy:
#!/usr/bin/env groovy
import com.example.Docker
def call(String imageName) {
return new Docker(this).buildDockerImage(imageName)
}
vars/dockerLogin.groovy:
#!/usr/bin/env groovy
import com.example.Docker
def call() {
return new Docker(this).dockerLogin()
}
vars/dockerPush.groovy:
#!/usr/bin/env groovy
import com.example.Docker
def call(String imageName) {
return new Docker(this).dockerPush(imageName)
}
Updated Jenkinsfile (using global library):
@Library('jenkins-shared-library') _
pipeline {
agent any
tools {
maven 'maven-3.9'
}
stages {
stage("build jar") {
steps {
script {
buildJar()
}
}
}
stage("build and push image") {
steps {
script {
buildImage 'nvastola/demo-app:jma-3.0'
dockerLogin()
dockerPush 'nvastola/demo-app:jma-3.0'
}
}
}
}
}
Now each step is explicit and reusable independently. Another pipeline might only need dockerLogin() and dockerPush() if they're not building images in Jenkins.
The Real Iteration Process (What My Commits Show)
The post so far makes it seem like I wrote clean code that mostly worked. That's not what happened.
Looking at my commit history, I made 7 commits in about 1 hour:
Initial commit— Basic structurefix docker credentials to push to private repo— Credential syntax struggledeleted brace that broke code buildImage— Syntax error (missing/extra})call docker build with param— Learning function parametersdocker groovy changes - extracting logic— Moving to Docker classmore docker groovy changes - splitting into separate steps— Refactoringfixed syntax error in buildImage.groovy— More syntax issues
That's one commit every 8–9 minutes. The cycle was: write code → push to GitLab → run Jenkins pipeline → see error → fix error → repeat.
The Brace Error That Cost Me 10 Minutes
I had written:
def buildDockerImage(String imageName) {
script.echo 'building the docker image...'
script.sh "docker build -t $imageName ."
} // ← Extra closing brace here
}
WorkflowScript: 14: unexpected token: } @ line 14, column 5.
}
^
I stared at this for 10 minutes. The error said "line 14" but my logic error was on line 13—I had indented script.sh incorrectly, making it look like it was inside the echo call, so I "fixed" it by adding an extra closing brace.
The actual fix was just proper indentation:
def buildDockerImage(String imageName) {
script.echo 'building the docker image...'
script.sh "docker build -t $imageName ."
}
Groovy doesn't care about indentation, but humans do. If your indentation is wrong, you'll add syntax errors trying to "fix" it.
The Credential ID Hunt
In my Jenkinsfile, I initially wrote:
credentialsId: 'docker-hub-repo'
could not find credentials entry with ID 'docker-hub-repo'
But I knew I had created Docker Hub credentials in Jenkins. I could see them in the UI with the description "Docker Hub credentials."
The problem: Jenkins credentials need the UUID, not the credential name. To find it:
- Jenkins → Manage Jenkins → Credentials
- Click "System" → "Global credentials"
- Click your credential
- Look at the browser URL:
credentials/store/system/domain/_/credential/0849fe1b-1263-4101-9fcf-1a7bb0b567d4/ - Copy that UUID
When you create credentials in Jenkins, you give them a name and description. But the pipeline needs the ID, which is this long UUID only visible in the URL. Documentation rarely shows this step clearly. Write down the UUID immediately when creating credentials — don't hunt for it later.
The First Successful Run
After 7 commits, multiple syntax errors, the credential ID hunt, and the password escaping issue, I finally got a clean pipeline run.
Started by user noah
...
[Pipeline] stage (build jar)
[Pipeline] echo
building the application for branch master
[Pipeline] sh
+ mvn package
[INFO] BUILD SUCCESS
[Pipeline] stage (build and push image)
[Pipeline] echo
building the docker image...
[Pipeline] sh
+ docker build -t nvastola/demo-app:jma-3.0 .
Successfully tagged nvastola/demo-app:jma-3.0
[Pipeline] withCredentials
Login Succeeded
[Pipeline] sh
+ docker push nvastola/demo-app:jma-3.0
jma-3.0: digest: sha256:abc123... size: 2421
Finished: SUCCESS
That "Finished: SUCCESS" hit different. Not because the pipeline worked—that was expected after fixing all the errors. But because I realized: this exact same build logic can now be used by 10 other projects. Just reference the shared library and call the functions. No copying code. No maintaining duplicates.
I verified on Docker Hub by navigating to nvastola/demo-app → Tags → confirmed jma-3.0 was there. I also pulled and ran the image locally to verify the entire pipeline worked end-to-end.
Part 7: Making the Library Available in Jenkins
With the code written, I needed to make it available to Jenkins pipelines. There are two ways.
Option 1: Global Library (For Everyone)
Jenkins → Manage Jenkins → System → Global Pipeline Libraries:
- Name:
jenkins-shared-library - Default version:
master - Retrieval method: Modern SCM → Git
- Repository URL:
https://gitlab.com/nvastola/jenkins-shared-library.git
In Jenkinsfile:
@Library('jenkins-shared-library') _
pipeline {
// Your pipeline
}
The _ (underscore) imports the library but doesn't assign it to a variable. This is a Groovy convention. I started with this approach because it seemed simpler—configure once in Jenkins UI, use everywhere.
Option 2: Project-Specific (For One Pipeline)
After using the global library for a while, I switched to project-specific for more control:
#!/usr/bin/env groovy
library identifier: 'jenkins-shared-library@master', retriever: modernSCM(
[$class: 'GitSCMSource',
remote: 'https://gitlab.com/nvastola/jenkins-shared-library.git',
credentialsId: 'b12881fa-58d1-4a47-828f-970217c47e67'])
pipeline {
// Your pipeline
}
Why I switched:
- Explicit versioning: Each Jenkinsfile declares exactly which library version it uses
- Testing: I can test library changes in one pipeline before affecting all pipelines
- Visibility: The Jenkinsfile shows all dependencies — no hidden global config
- Portability: If I move the Jenkinsfile to a different Jenkins instance, the library reference comes with it
Part 8: Versioning the Library
One powerful feature of Shared Libraries is versioning. You can reference specific versions in your Jenkinsfile:
@Library('jenkins-shared-library@v1.0.0') _ // Use tagged version
@Library('jenkins-shared-library@develop') _ // Use develop branch
@Library('jenkins-shared-library@abc123') _ // Use specific commit
Why this matters:
- Stability: Production pipelines can use
v1.0.0while dev pipelines testv2.0.0-beta - Rollback: If a library update breaks things, revert to the previous version
- Testing: New library features can be tested on non-critical pipelines first
In production, you'd use Git tags:
git tag -a v1.0.0 -m "Initial stable release"
git push origin v1.0.0
For this learning project, I'm using @master to always pull the latest code. In a real environment with multiple teams, I would tag stable releases.
Part 9: Integrating the Library (The First Real Test)
With the shared library built and available in Jenkins, it was time for the moment of truth: actually using it in a pipeline.
The First Function Call
Before (inline code):
stage("build jar") {
steps {
script {
echo "Building the application..."
sh 'mvn package'
}
}
}
After (shared library function):
stage("build jar") {
steps {
script {
buildJar()
}
}
}
Jenkins Console Output on first run:
Loading library jenkins-shared-library@master
...
[Pipeline] echo
building the application for branch master
[Pipeline] sh
+ mvn package
...
The buildJar() function from my shared library executed. Jenkins loaded the library, found the function, and ran it.
Adding More Functions
stage("build and push image") {
steps {
script {
buildImage 'nvastola/demo-app:jma-3.0'
dockerLogin()
dockerPush 'nvastola/demo-app:jma-3.0'
}
}
}
Notice the Groovy syntax: buildImage 'nvastola/demo-app:jma-3.0' works the same as buildImage('nvastola/demo-app:jma-3.0'). Groovy lets you omit parentheses for single-argument functions.
What About Deployment?
The deployment stage still calls gv.deployApp() from script.groovy rather than the shared library. This is intentional:
- Build logic is universal → Shared library
- Deployment logic is environment-specific → Local script
Deployment to dev vs staging vs production involves different servers, credentials, and configurations. That logic should live in the project repository where it can be customized per environment.
What I Learned (The Real Takeaways)
1. DRY Applies to Infrastructure Code Too
"Don't Repeat Yourself" isn't just for application code. Pipeline logic is code. If you're copying it, you're doing it wrong.
Before Shared Library:
- 10 microservices × 100 lines of pipeline code = 1,000 lines
- Change Docker registry? Update 10 files
After Shared Library:
- 10 microservices × 10 lines of function calls = 100 lines
- 1 shared library × 100 lines of implementation = 100 lines
- Total: 200 lines (80% reduction). Change Docker registry? Update 1 file.
2. The script Object Is Essential for Classes
Inside vars/ functions, Jenkins commands work directly. Inside src/ classes, you need the script object. Why? vars/ functions run in the Jenkins pipeline context automatically. Classes don't — they're just Groovy classes. You have to explicitly pass the Jenkins context.
// vars/myFunction.groovy
def call() {
return new MyClass(this).doSomething() // Pass 'this' as script
}
// src/com/example/MyClass.groovy
class MyClass implements Serializable {
def script
MyClass(script) { this.script = script }
def doSomething() { script.echo "Hi" }
}
3. Single Responsibility Principle for Functions
Splitting buildAndPushImage() into three separate functions made the library more flexible:
- ✅ Compose them however you need
- ✅ Test each independently
- ✅ Other projects can use just the parts they need
4. Versioning Gives You Safety
Being able to pin pipelines to specific library versions means you can test changes without breaking production, roll back if something goes wrong, and migrate gradually. This is critical in enterprise environments where stability matters.
Production Considerations (What's Still Missing)
What I Built (Learning)
- ✅ Basic shared library with reusable functions
- ✅ Organized code with classes
- ✅ Version control with Git
- ✅ Functions for build, Docker, and deploy
What Production Would Add
Testing:
- Unit tests for Groovy classes
- Integration tests for pipeline functions
- Automated testing in CI before merging changes
Documentation:
- README with function signatures and examples
- Inline documentation for parameters
- Changelog for version updates
Security:
- Separate libraries for different security levels
- Approval process for library changes
- Audit logging for who uses which functions
Organization:
- Separate libraries for different purposes (docker-library, maven-library, kubernetes-library)
- Team-specific libraries vs company-wide libraries
- Deprecation policy for old functions
How This Connects to Real Enterprise DevOps
In a company with 100+ microservices, 10+ teams, and multiple programming languages, you might have:
- Core library: Docker, notifications, security scanning (used by everyone)
- Language-specific libraries: Java/Maven, Python/pip, Node/npm
- Team-specific libraries: Payment team's deploy logic, Auth team's testing functions
The pattern scales:
jenkins-shared-library-core (v2.1.0)
├── Used by all 100 microservices
├── Managed by platform team
└── Changes require approval
jenkins-shared-library-payment (v1.5.0)
├── Used by payment microservices
├── Managed by payment team
└── Can change independently
Each team controls their own library but benefits from shared core functionality. This is how companies manage complexity at scale.
Reflection: The Learning Process
What worked:
- Starting with Freestyle jobs before jumping to pipelines
- Learning external Groovy scripts before shared libraries
- Writing simple functions in vars/ before adding src/ classes
- Using project-specific library first, then making it global
- Committing frequently (even broken code) to track progress
What I'd do differently:
- Read about the
scriptobject before writing classes (would have saved an hour) - Write down the credential UUID immediately instead of hunting for it later
- Document functions as I wrote them, not afterward
The Nexus Rebuild Lesson
Before this project, I deployed Nexus to a DigitalOcean droplet and manually configured it. Then I realized I'd set up the Docker repository wrong—it needed an HTTP connector, not HTTPS. My first instinct: rebuild the entire droplet from scratch.
Halfway through clicking through the Nexus UI for the second time, it hit me: I could have just run Nexus in Docker:
docker run -d \
-p 8081:8081 \
-p 8083:8083 \
-v nexus-data:/nexus-data \
sonatype/nexus3
Expose the ports during container creation, done. Configuration can be scripted or at least documented in a Dockerfile.
This is why Docker and infrastructure-as-code matter:
- Manual setup: One mistake = rebuild everything
- Docker: One mistake =
docker rm, change config,docker runagain
I spent 2 hours rebuilding manually when I could have spent 5 minutes with Docker. But you have to make the mistake to learn the lesson.
Why Docker Hub Instead of Nexus?
After the Nexus rebuild, I decided to keep Nexus for Maven artifacts (JAR files) but use Docker Hub for Docker images.
Reasoning:
- Learning opportunity: I wanted to learn how to push to both Nexus and Docker Hub from Jenkins
- Simplicity: Docker Hub doesn't require server management
- Cost: The Nexus droplet was temporary for learning — I wasn't going to keep paying $48/month for a personal project
- Separation of concerns: Maven artifacts in Nexus, Docker images in Docker Hub
In a real company, everything would probably go through Nexus (or Artifactory, or AWS ECR). But for learning, using different repositories for different artifact types taught me how to configure multiple destinations.
Time investment: ~4 hours in one afternoon (after a week of learning Jenkins fundamentals)
Cost: $48/month (DigitalOcean droplet: 4GB RAM, 4 vCPUs, 160GB storage for Jenkins)
The Nexus droplet from Project 2 was temporary for learning and has been shut down. For this project, I only kept the Jenkins droplet running and used Docker Hub (free) for image storage.
Breakdown (one afternoon, ~4 hours):
- 30 min: Creating shared library repository structure
- 45 min: Writing first functions (buildJar, buildImage)
- 1 hour: Learning and implementing the
scriptobject pattern for the Docker class - 1 hour: The 7-commit rapid iteration (syntax errors, credential UUID hunt, password escaping, refactoring)
- 45 min: Splitting into separate functions and organizing code
What took the longest: The 7-commit iteration hour. Syntax errors, finding the credential UUID, figuring out password escaping — lots of small issues that added up. The script object pattern itself was straightforward once I understood it from the tutorial.
The Pattern I Now Understand
Manual work (Projects 1–2): SSH to server, run commands by hand. Copy files with scp. Understand what deployment actually does.
Containerization (Project 3): Package apps with dependencies. Build → Tag → Push → Pull → Deploy. Simulate CI/CD workflow manually.
Automation (Project 4, earlier): Write Jenkinsfiles to automate the workflow. One pipeline per project. Still copying logic between projects.
Scaled Automation (This project): Extract common logic into reusable functions. One library, many projects. Change once, affect everywhere.
Next: AWS Services for cloud infrastructure — because running everything locally or on a single DigitalOcean droplet doesn't scale to production.
What's Next
With Jenkins pipelines automated and shared libraries handling common logic, the next challenge is AWS Services. I've been working locally and with DigitalOcean, but real enterprise infrastructure runs on AWS.
I'm expecting:
- EC2 instances and security groups
- S3 for storage
- IAM roles and permissions
- VPCs and networking
- More "why isn't this working?" moments with AWS-specific configurations
But I'm also seeing how all these pieces connect: Docker packages the apps, Jenkins builds and deploys them, Shared Libraries keep the automation DRY, and AWS will provide the scalable infrastructure to run it all.