Docker, microservices, and continuous delivery are three disciplines that reinforce each other particularly well. When an application is decomposed into dozens of independent services that communicate over HTTP, manually building, testing, and deploying each one quickly becomes unmanageable. Docker provides the isolation boundary each service needs, while Jenkins Pipeline automates the entire build-test-deploy sequence in a repeatable, auditable way.
This article walks through a working example: a small Spring Boot and Spring Cloud application composed of four services — account, customer, a Eureka discovery server, and a Zuul API gateway — each containerised with Docker and deployed through a dedicated Jenkins pipeline. The original sample code was authored by Piotr Minkowski and is available on GitHub at https://github.com/piomin/sample-spring-microservices.
Sample Microservices Architecture
The sample application contains four components, each running in its own Docker container:
- account-service — a REST API exposing account data, listening on port 2222.
- customer-service — a REST API exposing customer data, listening on port 3333.
- discovery-service — a Netflix Eureka server that acts as the service registry, listening on port 8761.
- gateway-service — a Netflix Zuul API gateway that proxies all inbound requests to the appropriate downstream service, listening on port 8765.
At startup, account-service and customer-service register themselves with the Eureka discovery server. Zuul resolves service addresses by querying Eureka, so the gateway never needs hard-coded downstream URLs. When more than one instance of a service is registered, Netflix Ribbon (embedded in Feign clients) automatically load-balances requests across them.
The services are implemented with Spring Boot and Spring Cloud, which provide out-of-the-box support for Eureka, Zuul, Ribbon, Feign, and Hystrix through starter dependencies declared in each module’s pom.xml. Detailed implementation notes for the Spring Cloud layer are outside the scope of this article; the focus here is the CI/CD pipeline.
Dockerfiles
Each service module contains a Dockerfile at its root. The pattern is identical across all four services: start from an OpenJDK base image, copy the fat JAR produced by Maven, and run it. Below is the Dockerfile for account-service:
FROM openjdk
MAINTAINER Piotr Minkowski <piotr.minkowski@gmail.com>
ADD target/account-service.jar account-service.jar
ENTRYPOINT ["java", "-jar", "/account-service.jar"]
EXPOSE 2222Code language: CSS (css)
For Docker to find a single, predictably named JAR in the target/ directory, the Maven build must strip the version suffix from the output filename. This is achieved by setting <finalName> in the module’s pom.xml and configuring spring-boot-maven-plugin to repackage the JAR with the correct main class:
<build>
<finalName>account-service</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.5.2.RELEASE</version>
<configuration>
<mainClass>pl.piomin.microservices.account.Application</mainClass>
<addResources>true</addResources>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>Code language: HTML, XML (xml)
The same Dockerfile pattern and Maven configuration applies to customer-service, discovery-service, and gateway-service, with only the service name, main class, and exposed port changing between them.
Jenkins Pipelines
The CI/CD automation uses the Jenkins Pipeline Plugin together with the Docker Pipeline Plugin (published by CloudBees). Four pipelines are defined — one per microservice — and they chain together sequentially: the discovery pipeline triggers the account pipeline on completion, the account pipeline triggers the customer pipeline, and the customer pipeline triggers the gateway pipeline.
Local Docker Registry
Before running the pipelines, spin up a local Docker registry on port 5000. Each pipeline pushes its freshly built image there, and the registry image name prefix (localhost:5000/) is how Docker knows to use it instead of Docker Hub.
docker run -d --name registry -p 5000:5000 registryCode language: CSS (css)
You can verify which images have been pushed by querying the registry’s REST API:
curl http://localhost:5000/v2/_catalogCode language: JavaScript (javascript)
Discovery Service Pipeline (Jenkinsfile)
The discovery service pipeline has five stages: Checkout, Build, Image, Run, and Final. Comments inline explain each decision:
node {
withMaven(maven:'maven') {
stage('Checkout') {
// Pull the latest code from the shared repository
git url: 'https://github.com/piomin/sample-spring-microservices.git',
credentialsId: 'github-piomin',
branch: 'master'
}
stage('Build') {
// Compile, test, and package all modules
sh 'mvn clean install'
// Read the version string from pom.xml so the Docker image
// tag stays in sync with the Maven artifact version
def pom = readMavenPom file:'pom.xml'
print pom.version
env.version = pom.version
}
stage('Image') {
dir ('discovery-service') {
// Build the Docker image and push it to the local registry
def app = docker.build "localhost:5000/discovery-service:${env.version}"
app.push()
}
}
stage ('Run') {
// Start the container, exposing the Eureka UI/API port
// and setting a hostname so other containers can link to it
docker.image("localhost:5000/discovery-service:${env.version}")
.run('-p 8761:8761 -h discovery --name discovery')
}
stage ('Final') {
// Kick off the next pipeline without blocking this one
build job: 'account-service-pipeline', wait: false
}
}
}Code language: PHP (php)
Account Service Pipeline (Jenkinsfile)
The account service pipeline is structurally identical. The key difference is in the Run stage: the container is started with --link discovery so that it can resolve the hostname discovery and register with the Eureka server on startup. It then triggers the customer service pipeline.
node {
withMaven(maven:'maven') {
stage('Checkout') {
git url: 'https://github.com/piomin/sample-spring-microservices.git',
credentialsId: 'github-piomin',
branch: 'master'
}
stage('Build') {
sh 'mvn clean install'
def pom = readMavenPom file:'pom.xml'
print pom.version
env.version = pom.version
}
stage('Image') {
dir ('account-service') {
def app = docker.build "localhost:5000/account-service:${env.version}"
app.push()
}
}
stage ('Run') {
// --link discovery makes the discovery container's hostname
// resolvable inside this container's /etc/hosts
docker.image("localhost:5000/account-service:${env.version}")
.run('-p 2222:2222 -h account --name account --link discovery')
}
stage ('Final') {
build job: 'customer-service-pipeline', wait: false
}
}
}Code language: PHP (php)
Equivalent Jenkinsfile scripts for customer-service and gateway-service follow the same pattern: each links to the containers started by its predecessor and triggers the next pipeline in the chain using wait: false. The Jenkinsfile for each service lives in that service’s sub-directory within the project repository.
End-to-End Testing
To exercise the full pipeline chain, trigger discovery-service-pipeline manually in Jenkins. It will complete its own stages and then automatically trigger account-service-pipeline, which triggers customer-service-pipeline, which triggers gateway-service-pipeline.
Once all four pipelines finish, run docker ps to verify that five containers are running: the local registry plus the four microservices.
CONTAINER ID IMAGE COMMAND PORTS NAMES
fa3b9e408bb4 localhost:5000/gateway-service:1.0-SNAPSHOT "java -jar /gatewa…" 0.0.0.0:8765->8765/tcp gateway
cc9e2b44fe44 localhost:5000/customer-service:1.0-SNAPSHOT "java -jar /custom…" 0.0.0.0:3333->3333/tcp customer
49657f4531de localhost:5000/account-service:1.0-SNAPSHOT "java -jar /accoun…" 0.0.0.0:2222->2222/tcp account
fe07b8dfe96c localhost:5000/discovery-service:1.0-SNAPSHOT "java -jar /discov…" 0.0.0.0:8761->8761/tcp discovery
f9a7691ddbba registry 0.0.0.0:5000->5000/tcp registryCode language: JavaScript (javascript)
Inspect individual container logs with docker logs <name>, for example:
docker logs account
With all services healthy you can call the account REST API directly:
curl http://localhost:2222/accountsCode language: JavaScript (javascript)
Or route the same request through the Zuul gateway:
curl http://localhost:8765/account/accountCode language: JavaScript (javascript)
Limitations and Next Steps
The pipeline described here is intentionally minimal. Several real-world concerns are left as exercises:
- Docker
--linkis a legacy feature. For production use, replace container linking with a user-defined Docker bridge network or move to Docker Compose / Kubernetes. - Single host. All containers run on the same machine. A realistic environment distributes services across multiple hosts, which requires an orchestrator such as Kubernetes or Docker Swarm and a proper service-discovery strategy.
- No automated integration tests. The pipeline builds and runs containers but does not yet verify service behaviour programmatically. Adding an integration-test stage after Run — one that calls each service’s health endpoint and asserts a 200 response — would make the pipeline genuinely production-ready.
- Horizontal scaling. Jenkins agents (slaves) running on separate machines or inside Docker containers can distribute build load and remove the single-point-of-failure risk of a single Jenkins master node.
- Image tagging strategy. The pipeline tags images with the Maven POM version. A common complement is to also tag with the Git commit SHA for unambiguous traceability.
Conclusion
This walkthrough demonstrates a practical, runnable continuous delivery pipeline for a four-service Spring Boot microservices system. The combination of per-service Dockerfiles, a local image registry, and chained Jenkins Pipeline scripts gives each service an independent build lifecycle while preserving a clear deployment order. As your system grows, the same principles scale: replace --link with proper networking, introduce Kubernetes for orchestration, add automated acceptance tests, and distribute Jenkins build agents for parallelism. The patterns shown here remain the foundation on which those improvements are built.