Zero‑Downtime SpringBoot Deployment by Sharing the Same Port
The article explains how to update a SpringBoot application without stopping the old process by detecting port usage, temporarily launching a new instance on an alternate port, then seamlessly switching the original port to the new instance, achieving near‑zero downtime.
Design Idea
When updating code on a personal or enterprise server, the usual approach is to stop the existing process because the new process needs the same port, which causes temporary unavailability for users.
A simple workaround is to start the new version on a different port, update the Nginx forwarding rule, restart Nginx quickly, and then stop the old process, but constantly changing ports and scripts is cumbersome.
The core idea is to let two SpringBoot processes share the same port by leveraging the embedded Servlet container (Tomcat) that SpringBoot uses. If Tomcat supports embedding, we can create a new Tomcat instance directly:
public class Main {
public static void main(String[] args) {
try {
Tomcat tomcat = new Tomcat();
tomcat.getConnector();
tomcat.getHost();
Context context = tomcat.addContext("/", null);
tomcat.addServlet("/", "index", new HttpServlet(){
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().append("hello");
}
});
context.addServletMappingDecoded("/", "index");
tomcat.init();
tomcat.start();
} catch (Exception e) {}
}
}In SpringBoot, the ServletWebServerFactory (e.g., TomcatServletWebServerFactory ) creates the embedded container. The factory provides start() and stop() methods via the WebServer interface.
The DispatcherServlet is not added directly; instead SpringBoot registers a ServletContainerInitializer implementation ( TomcatStarter ) that supplies a collection of ServletContextInitializer objects, one of which registers the DispatcherServlet with Tomcat.
During Tomcat startup, it calls back to SpringBoot, which iterates over the ServletContextInitializer collection to initialize the servlet context. The method getWebServer receives this collection as a parameter.
To obtain the initializer collection, SpringBoot uses ServletContextInitializerBeans :
protected static Collection
getServletContextInitializerBeans(ConfigurableApplicationContext context) {
return new ServletContextInitializerBeans(context.getBeanFactory());
}With these pieces in place, the seamless update process consists of:
Check whether the target port is already in use.
If it is, start the new SpringBoot instance on an alternative port.
Wait until the new instance is ready, then terminate the old process.
Create a new container instance, bind the original DispatcherServlet to it, and start the server on the original port.
This sequence makes the downtime virtually invisible (typically less than one second).
Implementation Code
@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)) {}
ServletWebServerFactory webServerFactory = getWebServerFactory(run);
((TomcatServletWebServerFactory) webServerFactory).setPort(defaultPort);
WebServer webServer = webServerFactory.getWebServer(invokeSelfInitialize((ServletWebServerApplicationContext) run));
webServer.start();
((ServletWebServerApplicationContext) run).getWebServer().stop();
} catch (IOException | InterruptedException ignored) {}
}
}
private static ServletContextInitializer invokeSelfInitialize(ServletWebServerApplicationContext context) {
try {
Method method = ServletWebServerApplicationContext.class.getDeclaredMethod("getSelfInitializer");
method.setAccessible(true);
return (ServletContextInitializer) method.invoke(context);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
private static boolean isPortInUse(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
return false;
} catch (IOException e) {
return true;
}
}
protected static Collection
getServletContextInitializerBeans(ConfigurableApplicationContext context) {
return new ServletContextInitializerBeans(context.getBeanFactory());
}
private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) {
String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
return context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
}Test Demo
A simple controller is created for testing:
@RestController
@RequestMapping("port/test")
public class TestPortController {
@GetMapping("test")
public String test() {
return "1";
}
}Package it as a JAR (v1), start it, and verify the endpoint with a tool such as "Cool Request". Then build a second version (v2) that returns "2". Start v2 without stopping v1; the tool shows that the service remains available, and after a brief moment the response switches to the new version, confirming near‑zero downtime.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.