Zero downtime deployment with SSE and long startup times
Published on
Generated by Gemini
Intro: Zero-Downtime Deployment for Spring Boot Apps with Docker Compose
Deploying updates to a live application is a critical process. For applications with long startup times and persistent connections like Server-Sent Events (SSE), the challenge is to deploy without interrupting active users. A standard restart is not an option—it would mean minutes of downtime and abruptly terminated user sessions.
This tutorial provides a complete, step-by-step guide to achieving true zero-downtime deployments for a Spring Boot application using Docker Compose. We will tackle two specific, real-world challenges:
- An application that takes 1.5 minutes to become fully healthy.
- Long-lived Server-Sent Events (SSE) connections that can last up to 15 minutes and must not be aborted during a new release.
Our solution combines the Blue/Green deployment strategy for seamless traffic switching with graceful connection draining to protect long-lived user connections.
The Core Concepts
1. Blue/Green Deployment
This strategy eliminates downtime by running two identical production environments, which we call “Blue” and “Green.”
- Blue: The current, stable version serving all live user traffic.
- Green: The new version of the application, running in standby.
The deployment process is simple and safe:
- Deploy the new “Green” version alongside the live “Blue” version.
- Wait for “Green” to fully start up and pass its health checks. During this time, it receives no user traffic.
- Once “Green” is healthy, a reverse proxy (like Nginx) instantly switches all traffic from “Blue” to “Green.”
- The old “Blue” environment is kept running for a while to handle existing connections and can be shut down later.
This method ensures a healthy instance is always available to handle requests, making the switch instantaneous from the user’s perspective.
2. Graceful Shutdown & Connection Draining
For applications with long-lived connections (like SSE, WebSockets, or long file uploads), simply stopping the old container is dangerous. A standard docker stop command would terminate all active SSE connections.
Graceful shutdown solves this. The process is:
- The reverse proxy stops sending new requests to the old container.
- The application is signaled to shut down gracefully. It stops accepting new connections but allows existing ones to continue until they terminate naturally.
- The container waits until the last connection is closed before it finally shuts down.
This ensures that a deployment does not interrupt or disconnect active users.
The Tutorial: A Step-by-Step Guide
Prerequisites
- Docker and Docker Compose installed.
- A containerized Spring Boot application.
- Your Spring Boot app must have the Actuator dependency to expose a
/actuator/healthendpoint.
Step 1: Enable Graceful Shutdown in Spring Boot
First, configure your application to handle connection draining. Add the following properties to your src/main/resources/application.properties (or application.yml):
# Enable graceful shutdown mode. The server will stop accepting new requests
# but wait for active ones to complete.
server.shutdown=graceful
# Set the maximum time to wait. This MUST be longer than your max SSE
# connection time (15m). We'll use 16 minutes to be safe.
spring.lifecycle.timeout-per-shutdown-phase=16m
Crucially, you must rebuild your application’s Docker image after adding these properties.
Step 2: Create the docker-compose.yml File
This file defines our three services: the Nginx reverse proxy and the Blue/Green instances of our app. Notice the healthcheck for handling the slow startup and the stop_grace_period to allow for connection draining.
# docker-compose.yml
version: '3.8'
services:
# The Reverse Proxy that directs traffic
nginx:
image: nginx:1.25-alpine
container_name: zdd_nginx
volumes:
# Mounts our dynamically generated Nginx config
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
depends_on:
app_blue:
condition: service_started
app_green:
condition: service_started
# "Blue" instance of your application
app_blue:
image: my-spring-boot-app:latest # ⬅️ Replace with your app image
container_name: app_blue
environment:
- SPRING_PROFILES_ACTIVE=blue
expose:
- "8080"
# This tells Docker to wait 16 mins before force-killing the container.
# It allows our Spring Boot graceful shutdown to complete.
stop_grace_period: 16m
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
# Grace period for the 1.5 min startup before checks begin
start_period: 90s
# "Green" instance of your application
app_green:
image: my-spring-boot-app:latest # ⬅️ Replace with your app image
container_name: app_green
environment:
- SPRING_PROFILES_ACTIVE=green
expose:
- "8080"
stop_grace_period: 16m # Must also have the grace period
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 90s
Step 3: Configure Nginx
Our Nginx configuration needs to be dynamic so our script can easily switch traffic. We’ll use a template that the script will populate.
- Create a directory named
nginx. - Inside it, create a file named
nginx.conf.template:
# nginx/nginx.conf.template
events {}
http {
# Defines a group of servers. Our script will point this to the
# active container (e.g., app_blue or app_green).
upstream spring_app {
server ${TARGET_HOST}:8080;
}
server {
listen 80;
location / {
# Route all traffic to our upstream group
proxy_pass http://spring_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;
# Headers needed for SSE to work through a proxy
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
}
}
}
Note: The final proxy headers are essential for ensuring SSE connections are not buffered or prematurely closed by Nginx.
Step 4: The Automated Deployment Script
This bash script automates the entire Blue/Green deployment and graceful shutdown process. Save it as deploy.sh in your project’s root directory.
#!/bin/bash
set -e
# This function polls the Docker health status of a container
wait_for_health() {
local container_name=$1
echo "--> Waiting for '$container_name' to be healthy..."
# Loop until the health check status is "healthy"
until [ "$(docker inspect -f {{.State.Health.Status}} $container_name)" == "healthy" ]; do
echo -n "."
sleep 3
done
echo "" # Newline for cleaner output
echo "--> '$container_name' is healthy!"
}
# 1. Determine which color is current and which is next
if docker ps --filter "name=app_blue" --filter "status=running" | grep -q 'app_blue'; then
CURRENT_COLOR="blue"
NEXT_COLOR="green"
else
CURRENT_COLOR="green"
NEXT_COLOR="blue"
fi
echo "✅ Current active version is: $CURRENT_COLOR"
echo "🚀 Deploying new version: $NEXT_COLOR"
# 2. Start the new container without touching the others
echo "--> Starting the '$NEXT_COLOR' container..."
# --no-deps prevents restarting Nginx or the other app
# --build ensures we use the latest code
docker-compose up -d --build --no-deps app_$NEXT_COLOR
# 3. Wait for the new container to pass its health check
wait_for_health app_$NEXT_COLOR
# 4. Update Nginx to route traffic to the new container
echo "--> Switching Nginx traffic to '$NEXT_COLOR'..."
export TARGET_HOST="app_$NEXT_COLOR"
envsubst '$TARGET_HOST' < nginx/nginx.conf.template > nginx/nginx.conf
# 5. Gracefully reload Nginx to apply the new config
docker-compose exec nginx nginx -s reload
echo "--> Traffic switched successfully!"
# 6. Stop the old container gracefully
echo "--> Sending graceful shutdown signal to '$CURRENT_COLOR' container..."
echo "--> It will remain alive to drain active SSE connections (up to 16 mins)."
docker-compose stop app_$CURRENT_COLOR
echo "✅ Zero-downtime deployment complete! The old container will exit once all connections are closed."
Finally, make the script executable: chmod +x deploy.sh.
Deployment Workflow
Your deployment process is now fully automated.
First-Time Setup
On the very first deployment, you need to establish a “Blue” version and configure Nginx to point to it.
# 1. Start only the blue container initially
docker-compose up -d --build app_blue
# 2. Run the deployment script to configure Nginx and bring up green
./deploy.sh
Subsequent Deployments
For all future updates, simply run the script:
./deploy.sh
The script will handle everything: it detects that “Green” is now the live version, brings up a new “Blue” instance, waits for it to be healthy, switches traffic, and gracefully shuts down the old “Green” instance, protecting all active users.