Deploying WAR and webapps
Introduction
A Tomcat install is useful only when applications are deployed. This chapter covers the WAR layout, four deploy methods—from copying a file into webapps/ to external docBase—Context path rules, ROOT mapping, and clean undeploy. Chapter 08 builds the WAR with Maven; here the focus is getting any WAR onto Tomcat and verifying it runs.
Prerequisites
- Installing Tomcat — Tomcat running
- web.xml and context.xml — Context fragments and
autoDeploy - Maven packaging basics — know what
warpackaging means
WAR Structure Recap
A WAR (Web Application Archive) is a ZIP with a fixed layout:
myapp.war
├── META-INF/
│ ├── MANIFEST.MF
│ └── context.xml ← optional
├── WEB-INF/
│ ├── web.xml ← optional (Servlet 3+)
│ ├── classes/ ← .class files (com/example/…)
│ └── lib/ ← application JARs
├── index.html ← public static files
└── css/, js/, …| Path | URL access |
|---|---|
/index.html (WAR root) | http://host:8080/myapp/index.html |
WEB-INF/web.xml | Not browsable |
WEB-INF/classes/ | Not browsable |
Code explanation:
- Context path
/myappcomes from the WAR namemyapp.warby default - Maven builds WARs to
target/myapp.waraftermvn package
Inspect a WAR without deploying:
jar tf target/myapp.war | head -20
unzip -l target/myapp.warContext Path and URL Mapping
| Deploy name | Default context path | Example URL |
|---|---|---|
hello.war | /hello | http://localhost:8080/hello/ |
hello/ (exploded dir) | /hello | same |
ROOT.war or ROOT/ | / (site root) | http://localhost:8080/ |
conf/Catalina/localhost/api.xml | /api | http://localhost:8080/api/ |
Code explanation:
- Context path is the first URL segment after host
- Servlet
@WebServlet("/users")insidehello.war→/hello/users
Override path in META-INF/context.xml (use carefully):
<Context path="/custom" />Prefer matching WAR name and URL expectations to avoid confusion.
Method 1: Copy WAR to webapps/ (Auto-Deploy)
Simplest path when autoDeploy="true" on the Host (default).
cp target/myapp.war $CATALINA_HOME/webapps/
# wait a few seconds
curl -I http://127.0.0.1:8080/myapp/Code explanation:
- Tomcat detects the new file, expands to
webapps/myapp/ifunpackWARs="true" - Logs:
logs/catalina.*.logandlogs/localhost.*.log
Upgrade:
$CATALINA_HOME/bin/shutdown.sh
rm -rf $CATALINA_HOME/webapps/myapp $CATALINA_HOME/webapps/myapp.war
cp target/myapp-2.war $CATALINA_HOME/webapps/myapp.war
$CATALINA_HOME/bin/startup.shTip
Stop Before Replace on Busy Systems
Hot-replacing WAR while running can work with autoDeploy but risks partial state. Shutdown → replace → start is safer for production.
Method 2: Exploded Directory
Unpack manually for inspection or quick static edits (learning only):
mkdir -p $CATALINA_HOME/webapps/myapp
cd $CATALINA_HOME/webapps/myapp
jar xf /path/to/myapp.warOr let Tomcat unpack by dropping myapp.war once, then edit webapps/myapp/.
Code explanation:
- Tomcat serves the directory directly—no separate WAR file required if folder exists
- Do not treat exploded
webapps/as your Git project—always rebuild from source and redeploy WAR for real work
Remove:
rm -rf $CATALINA_HOME/webapps/myapp
# also remove myapp.war if presentMethod 3: Manager Application
Tomcat ships Manager at /manager/html (if not removed).
Enable access
- Edit
conf/tomcat-users.xml— see web.xml and context - Restart Tomcat
- Open
http://127.0.0.1:8080/manager/html - Log in with
manager-guirole user
From the GUI:
- WAR file to deploy — upload local WAR
- Deploy — specify context path and optional XML
- Undeploy — remove application
Text API (scripting):
curl -u admin:password \
--upload-file target/myapp.war \
"http://127.0.0.1:8080/manager/text/deploy?path=/myapp&update=true"Code explanation:
manager-scriptrole required for text API- Restrict Manager to localhost or admin VPN—never expose weak credentials on the public internet
List deployed apps:
curl -u admin:password http://127.0.0.1:8080/manager/text/listMethod 4: External docBase (Production-Friendly)
Keep WARs outside CATALINA_HOME/webapps/ for cleaner upgrades and permissions.
Create conf/Catalina/localhost/myapp.xml:
<?xml version="1.0" encoding="UTF-8"?>
<Context docBase="/opt/apps/myapp.war"
reloadable="false"
unpackWARs="true" />Deploy workflow:
sudo mkdir -p /opt/apps
sudo cp target/myapp.war /opt/apps/myapp.war
sudo chown tomcat:tomcat /opt/apps/myapp.war
# restart Tomcat or touch context if configured
sudo systemctl restart tomcat
curl -I http://127.0.0.1:8080/myapp/Code explanation:
- File name
myapp.xml→ context/myapp docBase— absolute path to WAR or directory- Upgrade: replace
/opt/apps/myapp.war, restart Tomcat—Tomcat install dir untouched
ROOT at site root with external WAR
conf/Catalina/localhost/ROOT.xml:
<Context docBase="/opt/apps/frontend.war" reloadable="false" />Serves app at http://localhost:8080/ (no /myapp prefix).
Deploy Method Comparison
| Method | Pros | Cons |
|---|---|---|
| webapps/ copy | Fastest for learning | Mixes apps with Tomcat tree |
| Exploded dir | Easy to inspect files | Easy to drift from source control |
| Manager | Upload without SSH | Security risk if exposed |
| External docBase | Clean ops, CI-friendly | Extra path + fragment file |
Undeploy
| Method | Action |
|---|---|
| webapps/ | Stop Tomcat; delete myapp.war and myapp/ directory |
| Manager | Undeploy button or text /undeploy?path=/myapp |
| localhost fragment | Remove myapp.xml, delete WAR at docBase, restart |
Verify removal:
curl -I http://127.0.0.1:8080/myapp/ # expect 404
ls $CATALINA_HOME/webapps/Stale work/ cache (optional cleanup after undeploy):
rm -rf $CATALINA_HOME/work/Catalina/localhost/myappDeployment Checklist
After every deploy:
# 1. Log errors
tail -40 $CATALINA_HOME/logs/localhost.$(date +%Y-%m-%d).log
# 2. HTTP status
curl -I http://127.0.0.1:8080/myapp/
# 3. Context list (if Manager enabled)
curl -u admin:password http://127.0.0.1:8080/manager/text/list| Check | Pass |
|---|---|
No SEVERE stack traces in log | Clean startup lines for Context /myapp |
curl -I | 200, 302, or expected auth redirect—not 404 |
| Correct context | URL prefix matches design |
Common Deploy Failures
| Symptom | Cause | Fix |
|---|---|---|
| 404 on all app URLs | Wrong context path; deploy not finished | Check WAR name vs URL; read logs |
ClassNotFoundException: jakarta.servlet… | Tomcat 9 vs 10 mismatch | Tomcat 10 for Jakarta apps |
IllegalArgumentException: Invalid docBase | Bad path in localhost XML | Absolute path; file exists; permissions |
| App starts then stops | Missing dependency in WEB-INF/lib | Rebuild WAR; check mvn dependency:tree |
myapp.war not expanding | unpackWARs="false" or disk full | Check Host settings and disk space |
| Old version still served | Browser cache or duplicate deploy | Hard refresh; remove old webapps/myapp/ |
autoDeploy and Production
From server.xml and Connectors:
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="false">Code explanation:
autoDeploy="false"— Tomcat ignores new files inwebapps/until restart or Manager deploy- CI pipeline: copy WAR to
/opt/apps/,systemctl restart tomcat
Package-Managed Tomcat Paths
Debian/Ubuntu tomcat10:
sudo cp target/myapp.war /var/lib/tomcat10/webapps/
sudo systemctl restart tomcat10External fragment may live under /etc/tomcat10/Catalina/localhost/—check your distro layout with:
dpkg -L tomcat10 | grep CatalinaRelationship to Spring Boot
| Artifact | Deploy to standalone Tomcat |
|---|---|
java -jar app.jar | No — embedded server, not a WAR |
app.war built with SpringBootServletInitializer | Yes — same deploy methods as any WAR |
See Spring Boot WAR on Tomcat.
Post-Chapter Checklist
- Deployed a WAR to
webapps/and verified withcurl - Understand context path from WAR file name
- Know how
ROOT.warmaps to/ - Can write a
conf/Catalina/localhost/*.xmlfragment with externaldocBase - Know safe undeploy steps (remove WAR + exploded dir)
What Comes Next
| Chapter | Topic |
|---|---|
| 08 — First Servlet application | Build WAR with Maven from scratch |
| 12 — Nginx reverse proxy Tomcat | Public URL → Tomcat |
| 16 — Project: Maven WAR deploy | End-to-end exercise |
FAQ
Can I rename the WAR after deploy?
Not cleanly—context path is tied to the name. Undeploy, rename myapp.war, redeploy, or set path in localhost XML.
Deploy without restart?
autoDeploy may pick up new WARs; production should restart or use Manager with a documented procedure.
One Tomcat, how many apps?
Many—each WAR is a Context. Watch memory and thread limits; see JVM tuning.
webapps/myapp.war and webapps/myapp/ both exist?
Tomcat prefers the exploded directory if both exist in some states—remove both when redeploying to avoid stale classes.
Manager 403?
User lacks role, or remote access blocked by RemoteAddrValve in Manager context—use localhost or configure allow list.
WAR vs JAR?
WAR for Servlet containers (Tomcat). JAR with main for Spring Boot executable—different deploy path.