DEV Community

Mo Rizal
Mo Rizal

Posted on

How I Built A Blue Green Deployment Pipeline With Jenkins

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:

Github Repository

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:

  1. Jenkins, orchestrates the deployment process.

  2. Blue environment, runs one version of the application.

  3. Green environment, runs the other version of the application.

  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The endpoint then returns the configured version:

GET /version

v1
Enter fullscreen mode Exit fullscreen mode

Or after deploying v2 release:

GET /version

v2
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Like Blue, the application listens on port 8080 inside the container. Docker publishes it on a different host port:

8082
Enter fullscreen mode Exit fullscreen mode

The Green environment is defined by:

deploy/docker-compose.green.yml
Enter fullscreen mode Exit fullscreen mode

When Green is the active environment, Nginx routes application traffic to:

app-green:8080
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

And when Green has been successfully deployed and verified, Jenkins changes the upstream configuration:

upstream app {
    server app-green:8080;
}
Enter fullscreen mode Exit fullscreen mode

The pipeline does not simply reload Nginx after changing the configuration.

It first validates the configuration:

docker exec app-nginx nginx -t
Enter fullscreen mode Exit fullscreen mode

and only then reloads Nginx:

docker exec app-nginx nginx -s reload
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The health check then succeeds:

curl --fail http://127.0.0.1:8081/health

{"status":"ok"}
Enter fullscreen mode Exit fullscreen mode

After the initial deployment, a request through Nginx confirms that v1 is serving traffic:

curl http://localhost
Enter fullscreen mode Exit fullscreen mode
{
  "message": "Hello",
  "version": "v1",
  "hostname": "5f6a41482e36"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

It then builds a new image:

zero-downtime-app:6
Enter fullscreen mode Exit fullscreen mode

and starts the Green environment:

Container app-green Creating
Container app-green Created
Container app-green Starting
Container app-green Started
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The check succeeds:

{"status":"ok"}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Nginx is then reloaded successfully.

A request through the normal application entry point now returns v2:

curl http://localhost
Enter fullscreen mode Exit fullscreen mode
{
  "message": "Hello",
  "version": "v2",
  "hostname": "9a86f0f3486"
}
Enter fullscreen mode Exit fullscreen mode

The deployment is now complete, and Jenkins removes the old Blue container:

docker rm -f app-blue

app-blue
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The pipeline finishes with:

ERROR: script returned exit code 1
Finished: FAILURE
Enter fullscreen mode Exit fullscreen mode

Most importantly, the Switch Traffic stage is never executed.

The existing version therefore remains active:

curl http://localhost
Enter fullscreen mode Exit fullscreen mode
{
  "message": "Hello",
  "version": "v1",
  "hostname": "5f6a41482e36"
}
Enter fullscreen mode Exit fullscreen mode

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)