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

JAR vs WAR: When to Choose

ApproachCommand / deployBest when
Executable JARjava -jar app.jarNew APIs, Docker, cloud, microservices
WAR on external TomcatCopy 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.

text
  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:

xml
<packaging>war</packaging>

Add provided scope for embedded Tomcat (so it is not bundled inside the WAR):

xml
<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:

xml
<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-web still pulls Spring MVC, Jackson, etc.
  • provided Tomcat starter — compile against Servlet API; external Tomcat supplies runtime
  • spring-boot-maven-plugin still 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:

java
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:

java
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 (no main on deploy)
  • configure(...).sources(DemoApplication.class) — same entry as SpringApplication.run
  • Keeping main allows local run with mvn spring-boot:run if you temporarily use embedded Tomcat on classpath for dev—or run from IDE

Typical layout:

text
src/main/java/com/example/demo/
├── DemoApplication.java
├── ServletInitializer.java
└── controller/
    └── HelloController.java

Step 3: Sample Controller (Verify)

Step 4: application.properties for External Tomcat

src/main/resources/application.properties:

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=framework

If you set an additional context path (usually unnecessary when WAR name defines it):

properties
server.servlet.context-path=/api

Then public URL: http://host:8080/api/hello when WAR is ROOT, or /api/api/hello if WAR is api.waravoid double prefix. Common pattern:

WAR filecontext-path in propertiesPublic path to /hello
api.war(default empty)/api/hello
ROOT.warempty/hello
ROOT.war/api/api/hello

Code explanation:

  • forward-headers-strategy=framework — trust X-Forwarded-* from Nginx
  • External config via env vars still works: SPRING_DATASOURCE_URL, etc.

Step 5: Build WAR

bash
mvn clean package
ls -la target/api.war
jar tf target/api.war | grep WEB-INF/lib/tomcat

Expected: no embedded Tomcat JARs in WEB-INF/lib (or only provided artifacts excluded from WAR).

Step 6: Deploy to Tomcat

bash
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).log

Look for Spring Boot banner and Started DemoApplication.

Verify:

bash
curl http://127.0.0.1:8080/api/hello

Step 7: Nginx in Front (Optional)

nginx
location /api/ {
    proxy_pass http://127.0.0.1:8080/api/;
    include /etc/nginx/proxy_params;
}

See Nginx reverse proxy Tomcat.

Local Development Options

ModeHow
IDE / spring-boot
Temporarily remove provided on tomcat starter, or use JAR profile
Deploy WAR to local Tomcatmvn package + copy to webapps/—closest to prod
Executable JAR profileMaven <profile> with packaging>jar</packaging> for daily dev

Example Maven profile for JAR dev (optional):

xml
<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

ErrorCauseFix
ClassNotFoundException: jakarta.servlet…Tomcat 9 with Boot 3Use Tomcat 10.1+
NoSuchMethodError on Tomcat classesEmbedded Tomcat in WARspring-boot-starter-tomcat provided
404 on /helloWrong contextUse /api/hello if WAR is api.war
App starts in JAR but not WARMissing ServletInitializerAdd SpringBootServletInitializer subclass
SpringApplication cannot find sourcesWrong class in configure()Point to @SpringBootApplication class
502 from NginxTomcat down or wrong proxy_passLoopback curl first

Check logs/localhost.*.log for Spring stack traces.

Boot Features on External Tomcat

FeatureOn external Tomcat
Actuator /actuator/healthWorks if on classpath and exposed
DataSource / HikariCPFrom application.properties—not Tomcat JNDI
Static resourcessrc/main/resources/static as usual
Embedded-only assumptionsRare libs assume main()—read their docs

Traditional Spring MVC WAR (Non-Boot) — Brief

Older WARs without Boot:

  • web.xml registers DispatcherServlet, or Java WebApplicationInitializer
  • 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 starter provided
  • ServletInitializer extends SpringBootServletInitializer
  • mvn package → WAR deploys on Tomcat 10
  • REST endpoint reachable with correct context path
  • forward-headers-strategy set if behind Nginx

What Comes Next

ChapterTopic
15 — Tomcat Docker and multi-instanceContainers, CATALINA_BASE
19 — Project: Spring Boot WARFull exercise
Spring Boot packagingJAR 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?

kotlin
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.