I remember the first time I caused a production outage. It was a simple patch—fixing a typo in a product name. But during the redeploy, the application was down for three minutes. Three minutes doesn’t sound like much unless you’re a customer trying to place an urgent order. The loss of trust was immediate. That day I decided there had to be a better way. That’s when I discovered the Blue-Green deployment pattern. This is the story of how I implemented it with Spring Boot, Docker, and Nginx, and how you can do it too.
What is Blue-Green Deployment?
The idea is straightforward. You maintain two identical production environments, called Blue and Green. At any time, only one of them handles live traffic. The other stays idle, ready to receive the next release. When you deploy a new version, you push it to the idle environment. After thorough health checks, you switch the load balancer so that all new requests go to the updated environment. If something goes wrong, you flip back to the previous one. Users never see downtime.
Think of it as having a spare server that sits warm, waiting. The switch is an atomic operation—traffic moves instantly. Wouldn’t that make every release feel safer?
When should you not use Blue-Green?
This pattern comes at a cost. You need enough hardware to run two full stacks simultaneously. If your application holds state locally (like in-memory sessions), you must externalize that state first. Database changes must be backward compatible for two versions. If you can accept those constraints, Blue-Green gives you the fastest rollback in the business.
The Setup: Spring Boot, Docker, and Nginx
I built a simple product catalog API with Spring Boot. It uses PostgreSQL via Docker Compose and Flyway for migrations. Nginx sits in front, acting as a dynamic reverse proxy. The trick is that Nginx can reload its upstream configuration without dropping connections.
Step 1: The Spring Boot Application
I started from a basic Spring Boot project. The only special addition was the Actuator health endpoint. That endpoint is what my deployment script checks before it dares to switch traffic.
# application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://db:5432/products
username: user
password: pass
jpa:
hibernate:
ddl-auto: none
flyway:
enabled: true
The health endpoint at /actuator/health returns a simple 200 when the database is reachable and the app is ready.
Step 2: Dockerizing for Two Environments
I created two Docker Compose files, one for Blue and one for Green. They share the same image but run on different ports. The Blue environment listens on port 8081, Green on 8082. Each has its own database container.
# docker-compose-blue.yml
version: '3.8'
services:
app:
image: myapp:latest
ports:
- "8081:8080"
environment:
- SPRING_PROFILES_ACTIVE=blue
depends_on:
db:
condition: service_healthy
db:
image: postgres:15
environment:
POSTGRES_DB: products
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5
I repeated this for Green with port 8082 and a different database container name (e.g., db_green). The application profile blue or green slightly changes the welcome message so I can tell which environment is active.
Step 3: Nginx as the Traffic Director
Nginx needs to proxy requests to either Blue or Green. I used separate upstream configuration files and a main nginx.conf that includes the correct one. The deploy script swaps the included file and reloads Nginx.
# nginx.conf
http {
upstream backend {
# This file will be replaced during deployment
include /etc/nginx/upstream-blue.conf;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
}
}
}
upstream-blue.conf contains:
server 127.0.0.1:8081;
upstream-green.conf contains:
server 127.0.0.1:8082;
Why use separate files? Because nginx -s reload can swap the include file without interrupting ongoing requests. You don’t have to restart the whole Nginx process.
The Deployment Script
Automation was essential. I wrote a shell script that:
- Builds the new Docker image.
- Starts the idle environment (say Green) using
docker-compose -f docker-compose-green.yml up -d. - Waits until the health endpoint of the new environment returns 200.
- Copies the correct upstream file (e.g.,
upstream-green.conf) to Nginx’s config directory. - Runs
nginx -s reload. - Stops the old environment (Blue) after a cooldown period.
Here’s the heart of the script:
#!/bin/bash
ENV=$1 # 'blue' or 'green'
if [ "$ENV" == "green" ]; then
NEW_PORT=8082
OLD_PORT=8081
NEW_UPSTREAM="upstream-green.conf"
else
NEW_PORT=8081
OLD_PORT=8082
NEW_UPSTREAM="upstream-blue.conf"
fi
echo "Starting $ENV environment..."
docker-compose -f docker-compose-$ENV.yml up -d
echo "Waiting for health check..."
while ! curl -sf http://localhost:$NEW_PORT/actuator/health; do
sleep 2
done
echo "Switching traffic to $ENV..."
cp /etc/nginx/$NEW_UPSTREAM /etc/nginx/upstream-active.conf
nginx -s reload
echo "Stopping old environment..."
docker-compose -f docker-compose-$( [ "$ENV" = "blue" ] && echo "green" || echo "blue" ).yml down
I added a cooldown of 30 seconds between the switch and the shutdown to allow in-flight requests to complete. This is a personal touch I learned the hard way—once I killed the old stack too early and dropped a few requests.
Database Migrations Must Be Backward Compatible
This is the trickiest part. When Blue is live and Green is warming up, both environments share the same database (or at least the same schema). You cannot make a migration that drops a column or renames a table, because Blue still uses the old schema.
I use Flyway with forward-only, additive migrations. For example, if I need to rename a column, I add a new column and keep the old one until the next release. It sounds wasteful, but it saves outages.
Have you ever deployed a migration that broke production because of a slow schema change? I have. That’s why I now always write migrations as:
- Adding columns is safe (they can be null).
- Adding tables is safe.
- Renaming columns requires two releases: first add new column, second drop old.
- Dropping columns requires the same two-step process.
In my travel app, I added a sku column while keeping the old product_code column untouched. After the switch to Green, the new code reads sku. Blue ignored it. Two weeks later, I removed product_code in a second release.
Personal Experience: The Night I Almost Broke The Launch
I was responsible for deploying a Black Friday sale page. The team had used Blue-Green for months, but someone skipped the health check step. They ran the script manually and switched traffic while the new container hadn’t fully initialized. The entire sale page returned 503 errors for twelve seconds. Twelve seconds of panic.
After that, I added a loop that checks the health endpoint ten times with a pause. Only after all checks pass does the switch happen. I also added a rollback script that reverts the upstream config and stops the new environment. It’s just a matter of running deploy.sh blue again, but with the roles reversed.
That experience taught me that automation without proper verification is dangerous. The health check is your lifeline. Never skip it.
Putting It All Together
- Start with
docker-compose-blue.ymlanddocker-compose-green.ymlready. - Run both environments initially (or just Blue).
- Your Nginx points to Blue.
- When you want to release v2, build the new image.
- Run
deploy.sh green. The script will spin up Green, verify health, switch Nginx, then kill Blue. - To rollback, run
deploy.sh blue(assuming Blue still runs the old code or you redeploy the previous image).
I keep the previous image tagged with the version so I can redeploy it instantly.
Monitoring During the Switch
Even with automation, you should watch the logs. I open a terminal with tail -f /var/log/nginx/access.log and another with docker logs -f green-app. If I see errors, I can rollback manually within seconds.
Monitoring the health of both environments before the switch is critical. I also expose a custom Actuator health indicator that checks the database connectivity and a simple business logic test (e.g., can the app fetch a product by ID?). That makes the health check a true readiness probe.
Conclusion
Blue-Green deployment changed how I sleep before a release. Instead of breaking out in a cold sweat, I know I can ship any time I want—even at 2 PM on a Tuesday. The setup costs extra hardware, but the peace of mind is worth it.
I encourage you to try this pattern. Start small: put a simple Spring Boot app behind Nginx, add two Docker Compose files, and write a deployment script. Once you see that first zero-downtime switch, you’ll never go back.
If you found this helpful, like the article and share it with your team. Have questions or suggestions? Drop a comment below. Let’s keep the conversation going.
Happy deploying!
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva