Deploying a new application version can be simple when the application is small and downtime is acceptable.
An engineer can stop the current application, deploy the new version, and start it again. The problem appears when the application is expected to remain available while a new version is being released.
This creates a window where the application may become unavailable.
The problem becomes more visible when the new version also has an issue.
A deployment might complete successfully from the infrastructure's perspective, but the application itself could still be unhealthy. A container may start but fail its health check, the application may not respond correctly, or a configuration problem may only become visible after the new version receives traffic.
In this situation, simply replacing the old version is risky.
Deployment process needs a way to prepare the new version without immediately exposing it to users, verify that it is working, and only then switch traffic to it.
This is where blue-green deployment becomes useful.
Instead of replacing the running application directly, two environments are maintained:
Blue, the currently active version receiving production traffic
Green, the new version being prepared and validated
The new release is deployed to the inactive environment first. Once it passes the required checks, the reverse proxy switches traffic from the active environment to the new one.
The new application version can be started and tested while the existing version continues serving users. If the new version fails validation, traffic does not need to move at all. The existing environment can simply remain active.
Now Kubernetes is one way to implement this kind of deployment strategies. It provides powerful primitives for managing containers, service discovery, traffic routing, health checks, scaling, and automated rollouts.
However, Kubernetes also introduces additional infrastructure and operational complexity.
Not every application needs that level of orchestration, and not every company wants to operate a Kubernetes cluster.
For smaller applications with simpler operational requirements, a combination of Docker, reverse proxy, and a CI/CD tool can provide a much simpler alternative.
By the end of this project, we will have a Jenkins based deployment workflow that demonstrates:
Automated application testing and image building
Blue-green application environments
Health checks before traffic switching
Reverse proxy based traffic switching
SSH based remote deployment
Safe deployment path
What We Are Building
The complete implementation is available in repository bellow:
The goal of this project is to build a simple deployment environment where Jenkins can deploy a new application version alongside the currently running version, verify it, and then switch traffic to it.
The architecture consists of a Jenkins server and a separate deployment server.
The deployment server runs the application containers and the reverse proxy, while Jenkins acts as the automation layer responsible for executing the deployment workflow.
Architecture
There are four main pieces involved:
Jenkins, orchestrates the deployment process.
Blue environment, runs one version of the application.
Green environment, runs the other version of the application.
Nginx, receives user traffic and determines which environment should receive it.
Application
To demonstrate the deployment workflow, For this project, we use a small Go HTTP application.
The application does not contain any complex business logic because the main focus of this project is the deployment process, not the application itself.
The application exposes two endpoints:
GET /health
GET /version
Health Endpoint
The /health endpoint is used to determine whether the newly deployed application is ready to receive traffic.
A successful response returns HTTP 200 OK:
GET /health
HTTP/1.1 200 OK
OK
Jenkins uses this endpoint after starting the new environment.
Version Identification
The second endpoint /version, allows us to identify which application version is currently serving the request.
The application reads the version from the APP_VERSION environment variable:
APP_VERSION=v1
APP_VERSION=v2
The endpoint then returns the configured version:
GET /version
v1
Or after deploying v2 release:
GET /version
v2
This makes the deployment behavior easy to observe.
Instead of simply checking whether the application responds, we can verify exactly which version is receiving traffic.
Jenkins
Jenkins is the automation engine for the deployment process.
Instead of manually connecting to the deployment server and executing commands such as:
docker build
docker compose up
curl /health
Jenkins executes these steps as a repeatable pipeline.
The pipeline implemented in this project performs the following stages:
The repository contains separate Jenkinsfiles for the two deployment directions:
deploy/
├── Jenkinsfile.blue
└── Jenkinsfile.green
This is because the deployment target alternates between the two environments.
For example, Jenkinsfile.blue deploys the new image to the Blue environment and then switches Nginx to Blue. Jenkinsfile.green does the same thing for Green.
The pipeline also uses the Jenkins build number as the Docker image tag:
environment {
APP_NAME = 'zero-downtime-app'
IMAGE_TAG = "${BUILD_NUMBER}"
DEPLOY_DIR = '/home/ubuntu/projects/zero-downtime-jenkins'
}
This gives every Jenkins build its own image tag instead of continuously overwriting a generic latest image.
For example:
Build #1 → zero-downtime-app:1
Build #2 → zero-downtime-app:2
Build #3 → zero-downtime-app:3
This makes it easier to identify exactly which build is running in an environment.
Blue Environment
The Blue environment represents one side of the deployment pair.
In this project, the application runs inside a Docker container named:
app-blue
The application itself listens on port 8080 inside the container, while Docker publishes it on port 8081 on the deployment server.
The corresponding Docker Compose configuration is kept in:
deploy/docker-compose.blue.yml
This allows the Blue environment to be started independently from the Green environment.
When Blue is the active environment, Nginx routes application traffic to:
app-blue:8080
and is deployed using Jenkinsfile.blue.
Green Environment
The Green environment provides the second application slot.
It runs in a separate Docker container:
app-green
Like Blue, the application listens on port 8080 inside the container. Docker publishes it on a different host port:
8082
The Green environment is defined by:
deploy/docker-compose.green.yml
When Green is the active environment, Nginx routes application traffic to:
app-green:8080
and is deployed using Jenkinsfile.green.
Reverse Proxy
In this project we use Nginx, who acts as the entry point for application traffic.
Users do not need to know whether Blue or Green is currently active. They simply connect to the application through Nginx.
server {
listen 80;
location / {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Nginx uses an upstream configuration to determine which container should receive the traffic.
For example, when Blue is active:
upstream app {
server app-blue:8080;
}
And when Green has been successfully deployed and verified, Jenkins changes the upstream configuration:
upstream app {
server app-green:8080;
}
The pipeline does not simply reload Nginx after changing the configuration.
It first validates the configuration:
docker exec app-nginx nginx -t
and only then reloads Nginx:
docker exec app-nginx nginx -s reload
This is important because an invalid Nginx configuration should not be allowed to replace the currently working configuration.
The actual traffic switching logic is implemented directly in the Jenkins pipeline.
SSH Connection
Jenkins and the deployment environment are intentionally separated.
The Jenkins server does not run the application containers. Instead, Jenkins connects to the deployment server through SSH and executes the required deployment commands there.
The pipeline uses two Jenkins credentials:
deploy-host
ssh-vm1
The deploy-host credential provides the deployment server address, while ssh-vm1 is used by the Jenkins SSH agent for authentication.
For example, when Jenkins needs to deploy the Green environment, it connects to the deployment server and executes Docker Compose there:
APP_IMAGE_TAG=${IMAGE_TAG} \
docker compose \
-f docker-compose.green.yml \
up -d
The same SSH connection is also used for testing, health verification, and traffic switching.
This means Jenkins acts as the orchestrator, while the deployment server remains responsible for running the actual workload.
Result
The deployment workflow was tested through three scenarios: deploying the initial version (Blue), deploying the new version (Green), and handling a failed Green deployment.
Deploying v1
The first deployment starts the application in the Blue environment.
Jenkins runs the test, builds the Docker image, deploys app-blue, and verifies its health endpoint:
[Pipeline] { (Deploy Blue)
Container app-blue Creating
Container app-blue Created
Container app-blue Starting
Container app-blue Started
The health check then succeeds:
curl --fail http://127.0.0.1:8081/health
{"status":"ok"}
After the initial deployment, a request through Nginx confirms that v1 is serving traffic:
curl http://localhost
{
"message": "Hello",
"version": "v1",
"hostname": "5f6a41482e36"
}
At this point, the application is running successfully and Blue is the active environment.
Deploying v2
The next deployment uses the Green environment.
Jenkins first runs the application tests:
ok jenkins-zero-downtime 0.002s
It then builds a new image:
zero-downtime-app:6
and starts the Green environment:
Container app-green Creating
Container app-green Created
Container app-green Starting
Container app-green Started
At this point app-blue is still running. The new version is therefore deployed without first stopping the existing application.
Verifying v2
Before changing production traffic, Jenkins checks the new environment directly:
curl --fail http://127.0.0.1:8082/health
The check succeeds:
{"status":"ok"}
This confirms that the new container is responding successfully before it is exposed through Nginx.
Switching Traffic
After the health check succeeds, Jenkins updates the Nginx upstream configuration to point to Green:
upstream app {
server app-green:8080;
}
The configuration is validated before Nginx is reloaded:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Nginx is then reloaded successfully.
A request through the normal application entry point now returns v2:
curl http://localhost
{
"message": "Hello",
"version": "v2",
"hostname": "9a86f0f3486"
}
The deployment is now complete, and Jenkins removes the old Blue container:
docker rm -f app-blue
app-blue
What Happens When Deployment Fails?
The workflow was also tested with an intentionally broken application version.
In the failed branch, the /health behavior causes the existing Go unit test to fail:
--- FAIL: TestHealth (0.00s)
main_test.go:16: expected status 200, got 500
FAIL
FAIL jenkins-zero-downtime 0.002s
Because the test stage fails, Jenkins stops the deployment before creating or exposing the new version:
Build Image → SKIPPED
Deploy Green → SKIPPED
Verify Green → SKIPPED
Switch Traffic → SKIPPED
Stop Blue → SKIPPED
The pipeline finishes with:
ERROR: script returned exit code 1
Finished: FAILURE
Most importantly, the Switch Traffic stage is never executed.
The existing version therefore remains active:
curl http://localhost
{
"message": "Hello",
"version": "v1",
"hostname": "5f6a41482e36"
}
This demonstrates the failure behavior we wanted from the deployment workflow:
A release that fails validation does not automatically replace the currently running version.
The result is a simple deployment workflow that provides the core safety properties of blue-green deployment.
Conclusion
This project demonstrated how Jenkins, Docker, and Nginx can be combined to implement a simple blue-green deployment workflow without Kubernetes.
The new version is tested and verified before receiving traffic, while the existing version remains available during deployment. When validation fails, Jenkins stops the pipeline and leaves the current version untouched.
For smaller applications, this provides a simple and practical way to reduce deployment downtime and the risk of releasing a broken version.
The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the full source code in the repository below.
https://github.com/muhammadyulasfipahrizal/blue-green-jenkins.git







Top comments (0)