Zero‑Downtime Spring Boot Deployment: Sharing a Port Between Two Instances
This article explains how to update a Spring Boot application without stopping the old process by letting a new instance start on the same port, detailing the Tomcat internals, the required ServletContainerInitializer callbacks, and providing a complete, step‑by‑step implementation with code samples and a live test.
Problem Statement
When updating code on a personal or enterprise server, the usual approach is to stop the existing process before starting the new one because both processes would try to bind to the same port. This causes a period of unavailability that depends on the application’s startup time, which is unacceptable for high‑traffic monolithic services.
Typical Work‑Around and Its Drawbacks
A common workaround is to start the new version on a different port, quickly switch the nginx upstream to the new port, and then terminate the old process. Although this avoids downtime, it requires frequent port changes and additional scripting, which can be cumbersome.
Design Idea
The solution leverages several Spring Boot source‑level mechanisms:
Understanding how Spring Boot embeds a Servlet container (Tomcat) via org.apache.catalina.startup.Tomcat.
How DispatcherServlet is handed to the container through the ServletContainerInitializer interface.
Retrieving the collection of ServletContextInitializer beans that Spring Boot registers.
By creating a new Tomcat instance, attaching the same DispatcherServlet, and then swapping the old instance, the application can be refreshed without any perceptible downtime.
Key Tomcat and Spring Boot Internals
Spring Boot creates a Tomcat instance via TomcatServletWebServerFactory. The factory’s getWebServer method receives a collection of ServletContextInitializer objects, among which one registers the DispatcherServlet. The container calls back through ServletContainerInitializer (implemented by TomcatStarter) to perform this registration.
To obtain the initializer collection, Spring Boot uses the helper class ServletContextInitializerBeans, which simply wraps the bean factory.
protected static Collection<ServletContextInitializer> getServletContextInitializerBeans(ConfigurableApplicationContext context) {
return new ServletContextInitializerBeans(context.getBeanFactory());
}Implementation Steps
Check whether the default port (e.g., 8088) is already in use.
If it is, start the new Spring Boot instance on an alternative port (e.g., 9090) by appending --server.port=9090 to the argument list.
After the new instance reports successful startup, kill the process that occupies the default port using a shell command that extracts the PID via lsof and sends kill -9.
Retrieve the original TomcatServletWebServerFactory from the running context, set its port back to the default, and create a new WebServer by invoking getWebServer with the original DispatcherServlet (obtained via reflection on the private getSelfInitializer method).
Start the newly created WebServer, then stop the old one.
Steps three to five happen within a few milliseconds, achieving near‑zero downtime.
Core Code Samples
Simple Tomcat bootstrap example (illustrates the low‑level API used by Spring Boot):
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) {}
}
}Method Spring Boot uses to obtain the appropriate ServletWebServerFactory:
private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) {
String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
return context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}Full implementation that performs the zero‑downtime switch:
@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<ServletContextInitializer> 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);
}
}Simple controller used for the demo:
@RestController
@RequestMapping("port/test")
public class TestPortController {
@GetMapping("test")
public String test() {
return "1";
}
}Testing the Zero‑Downtime Switch
1. Build the first version (v1) into a JAR and start it.
2. Verify the endpoint /port/test/test returns 1 using the "Cool Request" plugin in IDEA.
3. Build a second version (v2) where the controller returns 2.
4. Launch the v2 JAR without stopping v1; the code automatically detects the occupied default port, starts on 9090, then swaps the ports as described.
5. During the swap, the service is unavailable for less than one second. After the swap, the same endpoint now returns 2, confirming that the new code took effect with negligible downtime.
The screenshots (omitted here) show the request responses before and after the switch, illustrating the seamless transition.
Java Web Project
Focused on Java backend technologies, trending internet tech, and the latest industry developments. The platform serves over 200,000 Java developers, inviting you to learn and exchange ideas together. Check the menu for Java learning resources.
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.
