How to Run Multiple SpringBoot Microservices on a Single Tomcat with Minimal Resources

This guide explains how to adapt a distributed micro‑service architecture to run all SpringBoot services inside one external Tomcat instance by changing packaging to WAR, configuring server.xml, setting memory options, adjusting virtual addresses, handling logs, and registering services with Nacos for low‑memory environments.

Architect's Alchemy Furnace
Architect's Alchemy Furnace
Architect's Alchemy Furnace
How to Run Multiple SpringBoot Microservices on a Single Tomcat with Minimal Resources

Background

When your system adopts a distributed micro‑service architecture designed for massive data, high concurrency, and high throughput, you split many services and use numerous distributed middleware to decouple business.

Design looks beautiful, implementation is skinny!

Problems arise:

Engineers report server memory reaching 95%.

Product managers say the configured servers are too expensive and need to be downsized.

Developers claim services crash because resources are insufficient.

It feels like using a cannon to kill a chicken.

How can the same framework support both hundreds of millions of capacity with horizontal scaling and also run on a single PC with 8 GB / 16 GB memory?

Solution Overview

Principle: keep the underlying architecture unchanged, modify the packaging method, and run all services in a single external Tomcat process – a "beautiful transformation".

1. Tomcat Version Selection

SpringBoot 2.1.6.RELEASE recommends Tomcat 9; the latest version is 9.0.106. Download URL:

https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.106/bin/apache-tomcat-9.0.106-windows-x64.zip

2. Tomcat Configuration Changes

2.1 server.xml support multiple ports and services

<?xml version="1.0" encoding="UTF-8"?>
<!-- Apache License omitted for brevity -->
<Server port="8005" shutdown="SHUTDOWN">
    <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
    <!-- APR connector and OpenSSL support using Tomcat Native -->
    <Listener className="org.apache.catalina.core.AprLifecycleListener"/>
    <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
    <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
    <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
    <GlobalNamingResources>
        <Resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml"/>
    </GlobalNamingResources>
    <!-- Example service -->
    <Service name="pbp-auth-server">
        <Connector port="27020" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="28020" maxParameterCount="1000"/>
        <Engine name="pbp-auth-server" defaultHost="pbp-auth-server">
            <Realm className="org.apache.catalina.realm.LockOutRealm"/>
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
            <Host name="pbp-auth-server" appBase="pbp-auth-server" unpackWARs="true" autoDeploy="true">
                <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t \"%r\" %s %b"/>
            </Host>
        </Engine>
    </Service>
    <!-- Additional services omitted for brevity -->
</Server>

2.2 Tomcat memory configuration

Add setenv.bat in tomcat\bin:

set "JAVA_HOME=C:\Program Files\Java\jre1.8.0_311"
set "CATALINA_OPTS=-Xms512m -Xmx8192m -Dspring.profiles.active=dev"
set JAVA_OPTS=-Xms1024m -Xmx8192m -XX:PermSize=256M -XX:MaxNewSize=500m -XX:MaxPermSize=500m -Djava.awt.headless=true

2.3 Service virtual address (key point)

Each service is stored in its own folder; ensure Service Name == appBase == Host Name == folder name.

Expect to access each service via http://127.0.0.1:servicePort/ rather than through a context path that would cause Tomcat to restart.

In Tomcat, "/" refers to the ROOT folder of the appBase.

Therefore we name the folder ROOT under each appBase so that the service can be accessed directly by its port.

http://127.0.0.1:27060/v1/user/insert
http://127.0.0.1:27010/pbp-system/v1/user/insert

2.4 Log storage

Tomcat logs are under logs in the root directory.

When started via startup.bat, runtime logs appear in bin\logs.

When installed as a Windows service ( service.bat install), logs are in logs\serviceName.

3. SpringBoot Microservice Refactoring

3.1 Change packaging to WAR

<packaging>war</packaging>
<build>
    <finalName>ROOT</finalName>
</build>

3.2 Exclude embedded Tomcat

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
</dependency>

3.3 Extend SpringBootServletInitializer

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableFeignClients
@EnableDiscoveryClient
@EnableScheduling
@MapperScan(basePackages="com.pilot.basic.history.mapper")
@TableShardModelScan(basePackages={"com.pilot.basic.history.entity.model"})
@ConsumerListenerScan
public class BasicHistoryApplication extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(BasicHistoryApplication.class, args);
    }
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(BasicHistoryApplication.class);
    }
}

3.4 Nacos registration for WAR deployment

@Component
@Slf4j
public class NacosRegisterOnWar implements ApplicationRunner {
    @Autowired
    private NacosRegistration registration;
    @Autowired
    private NacosAutoServiceRegistration nacosAutoServiceRegistration;
    @Value("${server.port}") Integer port;
    @Value("${spring.application.name:pbp-history-data}") String applicationName;
    @Override
    public void run(ApplicationArguments args) throws Exception {
        if (registration != null && port != null) {
            Integer tomcatPort = port;
            try {
                tomcatPort = Integer.valueOf(getTomcatPort());
            } catch (Exception e) {
                log.warn("Failed to get external Tomcat port:", e);
            }
            registration.setPort(tomcatPort);
            nacosAutoServiceRegistration.start();
        }
    }
    public String getTomcatPort() throws Exception {
        MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();
        Set<ObjectName> objectNames = beanServer.queryNames(
            new ObjectName(applicationName + ":type=Connector,*"),
            Query.match(Query.attr("protocol"), Query.value("HTTP/1.1")));
        String port = objectNames.iterator().next().getKeyProperty("port");
        log.info("External Tomcat port: {}", port);
        return port;
    }
}

3.5 Additional configuration

Set a unique JMX domain for each Tomcat service to avoid conflicts:

spring:
  jmx:
    default-domain: pbp-io-server

4. Result

Running on a machine with 16 GB memory shows stable logs and successful service registration.

Architecture diagram
Architecture diagram
Running result
Running result
JavadeploymentNacosSpringBoottomcat
Architect's Alchemy Furnace
Written by

Architect's Alchemy Furnace

A comprehensive platform that combines Java development and architecture design, guaranteeing 100% original content. We explore the essence and philosophy of architecture and provide professional technical articles for aspiring architects.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.