Zero‑Downtime Spring Boot Deployment: Sharing a Single Port Across Versions
This article explains how to update a Spring Boot application without stopping the service by allowing two processes to share the same port, detailing the underlying Tomcat embedding, DispatcherServlet handling, and providing step‑by‑step code and a live demo to achieve seamless zero‑downtime deployments.
Problem
When updating a Spring Boot application on a personal or enterprise server, the usual practice is to stop the existing process before starting a new one because both processes need the same TCP port. This results in a brief outage, which is especially problematic for monolithic applications serving many concurrent users.
Design Idea
The approach leverages Spring Boot’s embedded Tomcat and the servlet container lifecycle to start a new instance on an alternative port while the old instance continues to serve traffic. After the new instance is fully started, the old process is terminated, the original port is reclaimed, and a new web server bound to the original port is started, achieving a seamless switch with near‑zero downtime.
Key Technical Steps
Detect whether the default port (e.g., 8088) is already in use.
If the port is occupied, launch the new Spring Boot instance on a temporary port (e.g., 9090) while keeping the old instance running.
When the new instance reports that it is ready, kill the old process (using lsof / kill on Unix‑like systems).
Reconfigure the TomcatServletWebServerFactory to use the original port, obtain a new WebServer from the factory, and start it.
Stop the web server that was created for the temporary port, completing the hand‑over.
Implementation
import org.apache.catalina.startup.Tomcat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import java.io.IOException;
import java.net.ServerSocket;
import java.lang.reflect.Method;
import java.util.Collection;
@SpringBootApplication
@EnableScheduling
public class WebMainApplication {
public static void main(String[] args) {
String[] newArgs = args.clone();
int defaultPort = 8088;
boolean needChangePort = false;
if (isPortInUse(defaultPort)) {
newArgs = new String[args.length + 1];
System.arraycopy(args, 0, newArgs, 0, args.length);
newArgs[newArgs.length - 1] = "--server.port=9090";
needChangePort = true;
}
ConfigurableApplicationContext run = SpringApplication.run(WebMainApplication.class, newArgs);
if (needChangePort) {
String command = String.format(
"lsof -i :%d | grep LISTEN | awk '{print $2}' | xargs kill -9", defaultPort);
try {
Runtime.getRuntime().exec(new String[]{"sh", "-c", command}).waitFor();
while (isPortInUse(defaultPort)) {
// wait until the old process releases the port
}
ServletWebServerFactory factory = getWebServerFactory(run);
((TomcatServletWebServerFactory) factory).setPort(defaultPort);
WebServer webServer = factory.getWebServer(invokeSelfInitialize((ServletWebServerApplicationContext) run));
webServer.start();
((ServletWebServerApplicationContext) run).getWebServer().stop();
} catch (IOException | InterruptedException ignored) {
// handle errors as needed
}
}
}
private static boolean isPortInUse(int port) {
try (ServerSocket ss = new ServerSocket(port)) {
return false;
} catch (IOException e) {
return true;
}
}
private static ServletContextInitializer invokeSelfInitialize(ServletWebServerApplicationContext ctx) {
try {
Method m = ServletWebServerApplicationContext.class.getDeclaredMethod("getSelfInitializer");
m.setAccessible(true);
return (ServletContextInitializer) m.invoke(ctx);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext ctx) {
String[] names = ctx.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
return (ServletWebServerFactory) ctx.getBeanFactory().getBean(names[0], ServletWebServerFactory.class);
}
}Demo and Verification
A simple controller returning "1" is packaged as v1.jar and started. A second version returning "2" is built as v2.jar. The first version runs on the default port. When v2.jar is launched, the application detects that port 8088 is occupied, starts on 9090, and once ready the script kills the old process, switches the new instance back to 8088, and stops the temporary server. The whole hand‑over takes less than one second, resulting in negligible downtime.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
