Spring Boot WAR on External Tomcat
Introduction
Spring Boot 3 defaults to an executable JAR with embedded Tomcat—java -jar app.jar and done. Some teams must deploy a WAR to a shared Tomcat instance (ops standard, multiple apps on one JVM, legacy data center rules). This chapter converts a Boot web app to WAR packaging, excludes embedded Tomcat from the WAR, extends SpringBootServletInitializer, and deploys to Tomcat 10.1—bridging the Spring Boot track and standalone Tomcat operations.
Prerequisites
- Your First Servlet Application — WAR deploy basics
- Deploying WAR and webapps
- A working Spring Boot 3 project with
spring-boot-starter-web(from Create First Spring Boot Project) - Tomcat 10.1.x + JDK 17+ (Jakarta)
JAR vs WAR: When to Choose
| Approach | Command / deploy | Best when |
|---|---|---|
| Executable JAR | java -jar app.jar | New APIs, Docker, cloud, microservices |
| WAR on external Tomcat | Copy WAR to webapps/ | Shared Tomcat ops, existing Java EE deploy pipeline, multiple WARs per server |
Both are valid for Spring Boot 3. Hello Code learning usually starts with JAR; this chapter teaches the WAR path for completeness and enterprise-style deploys.
JAR: app.jar ⊃ embedded Tomcat ⊃ Spring Boot
WAR: Tomcat (shared) ⊃ app.war ⊃ Spring Boot (no embedded Tomcat JARs)Step 1: Change pom.xml to WAR
In your Boot project pom.xml:
<packaging>war</packaging>Add provided scope for embedded Tomcat (so it is not bundled inside the WAR):
<dependencies>
<!-- existing starters, e.g. spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>Optional: set WAR file name to match context path:
<build>
<finalName>api</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>Produces target/api.war → context /api.
Code explanation:
spring-boot-starter-webstill pulls Spring MVC, Jackson, etc.providedTomcat starter — compile against Servlet API; external Tomcat supplies runtimespring-boot-maven-pluginstill repackages—WAR remains Boot-aware
Warning
Do Not Skip provided Tomcat
Without provided, embedded Tomcat JARs land in WEB-INF/lib and conflict with the container’s Tomcat classes.
Step 2: SpringBootServletInitializer
Create or update your application class:
package com.example.demo;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DemoApplication.class);
}
}Keep your existing main class:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}Code explanation:
ServletInitializer— Tomcat invokes this when the WAR starts (nomainon deploy)configure(...).sources(DemoApplication.class)— same entry asSpringApplication.run- Keeping
mainallows local run withmvn spring-boot:runif you temporarily use embedded Tomcat on classpath for dev—or run from IDE
Typical layout:
src/main/java/com/example/demo/
├── DemoApplication.java
├── ServletInitializer.java
└── controller/
└── HelloController.javaStep 3: Sample Controller (Verify)
Step 4: application.properties for External Tomcat
src/main/resources/application.properties:
# Optional context path inside the WAR deploy name
# If WAR is api.war, URLs are /api/hello — context comes from WAR name unless server.servlet.context-path set
server.forward-headers-strategy=frameworkIf you set an additional context path (usually unnecessary when WAR name defines it):
server.servlet.context-path=/apiThen public URL: http://host:8080/api/hello when WAR is ROOT, or /api/api/hello if WAR is api.war—avoid double prefix. Common pattern:
| WAR file | context-path in properties | Public path to /hello |
|---|---|---|
api.war | (default empty) | /api/hello |
ROOT.war | empty | /hello |
ROOT.war | /api | /api/hello |
Code explanation:
forward-headers-strategy=framework— trustX-Forwarded-*from Nginx- External config via env vars still works:
SPRING_DATASOURCE_URL, etc.
Step 5: Build WAR
mvn clean package
ls -la target/api.war
jar tf target/api.war | grep WEB-INF/lib/tomcatExpected: no embedded Tomcat JARs in WEB-INF/lib (or only provided artifacts excluded from WAR).
Step 6: Deploy to Tomcat
export CATALINA_HOME=/opt/tomcat
$CATALINA_HOME/bin/shutdown.sh
rm -rf $CATALINA_HOME/webapps/api $CATALINA_HOME/webapps/api.war
cp target/api.war $CATALINA_HOME/webapps/
$CATALINA_HOME/bin/startup.sh
tail -f $CATALINA_HOME/logs/catalina.$(date +%Y-%m-%d).logLook for Spring Boot banner and Started DemoApplication.
Verify:
curl http://127.0.0.1:8080/api/helloStep 7: Nginx in Front (Optional)
location /api/ {
proxy_pass http://127.0.0.1:8080/api/;
include /etc/nginx/proxy_params;
}See Nginx reverse proxy Tomcat.
Local Development Options
| Mode | How |
|---|---|
| IDE / spring-boot | Temporarily remove provided on tomcat starter, or use JAR profile |
| Deploy WAR to local Tomcat | mvn package + copy to webapps/—closest to prod |
| Executable JAR profile | Maven <profile> with packaging>jar</packaging> for daily dev |
Example Maven profile for JAR dev (optional):
<profiles>
<profile>
<id>jar</id>
<properties>
<packaging.type>jar</packaging.type>
</properties>
</profile>
</profiles>Many teams use JAR for dev, WAR for release—pick one standard per team.
Common Errors
| Error | Cause | Fix |
|---|---|---|
ClassNotFoundException: jakarta.servlet… | Tomcat 9 with Boot 3 | Use Tomcat 10.1+ |
NoSuchMethodError on Tomcat classes | Embedded Tomcat in WAR | spring-boot-starter-tomcat provided |
404 on /hello | Wrong context | Use /api/hello if WAR is api.war |
| App starts in JAR but not WAR | Missing ServletInitializer | Add SpringBootServletInitializer subclass |
SpringApplication cannot find sources | Wrong class in configure() | Point to @SpringBootApplication class |
| 502 from Nginx | Tomcat down or wrong proxy_pass | Loopback curl first |
Check logs/localhost.*.log for Spring stack traces.
Boot Features on External Tomcat
| Feature | On external Tomcat |
|---|---|
Actuator /actuator/health | Works if on classpath and exposed |
| DataSource / HikariCP | From application.properties—not Tomcat JNDI |
| Static resources | src/main/resources/static as usual |
| Embedded-only assumptions | Rare libs assume main()—read their docs |
Traditional Spring MVC WAR (Non-Boot) — Brief
Older WARs without Boot:
web.xmlregistersDispatcherServlet, or JavaWebApplicationInitializer- Same
webapps/deploy steps as this chapter - More XML and manual dependency management—why Boot defaults to JAR
This Tomcat track does not expand full pre-Boot Spring; see Spring Framework docs if you maintain legacy WARs.
Post-Chapter Checklist
-
packaging>war</packaging>and Tomcat starterprovided -
ServletInitializerextendsSpringBootServletInitializer -
mvn package→ WAR deploys on Tomcat 10 - REST endpoint reachable with correct context path
-
forward-headers-strategyset if behind Nginx
What Comes Next
| Chapter | Topic |
|---|---|
| 15 — Tomcat Docker and multi-instance | Containers, CATALINA_BASE |
| 19 — Project: Spring Boot WAR | Full exercise |
| Spring Boot packaging | JAR deploy and ops |
FAQ
Can I still run java -jar after WAR changes?
Only if you keep a JAR build profile or separate module. Pure WAR without embedded Tomcat is for external container deploy.
One WAR per Tomcat or many?
Tomcat supports many WARs—watch shared heap (JVM tuning).
Gradle instead of Maven?
plugins { war }
dependencies {
providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")
}Same SpringBootServletInitializer pattern.
Spring Boot 2 on Tomcat 9?
Boot 2 uses javax.* + Tomcat 9. This chapter is Boot 3 + Tomcat 10 + Jakarta.
Who configures DataSource—Tomcat or Boot?
Boot via application.yml. Tomcat JNDI is optional legacy—see web.xml and context.
Actuator on production Tomcat?
Expose only needed endpoints; restrict via Nginx or management.endpoints.web.exposure.