Apache Tomcat Overview

The reference-grade Servlet Container implementation — directory layout, architecture, deployment, and where Virtual Threads fit into a standalone install

← Back to Index

What is Apache Tomcat — and Why Does it Matter Specifically?

The previous page described the Servlet Container as an abstract contract — a specification any runtime can implement. Apache Tomcat is the concrete, most widely deployed implementation of that contract: an open-source project under the Apache Software Foundation implementing the Jakarta Servlet, Jakarta Server Pages, and Jakarta WebSocket specifications. Writing a compliant @RestController or a raw HttpServlet is only half the picture — something has to actually be the running process that hosts it, and for the large majority of Java web applications today, that something is Tomcat, whether installed standalone or embedded inside a Spring Boot JAR.

// BEFORE — a perfectly correct, compliant Servlet with nothing running it
public class OrderStatusServlet extends HttpServlet { /* ... */ }
// Compiled, packaged into a WAR — and going nowhere until some concrete
// implementation of the Servlet spec agrees to load and run it.

// AFTER — Tomcat is that concrete implementation
$ ./bin/startup.sh
$ cp target/myapp.war /opt/tomcat/webapps/
// Tomcat detects the WAR, extracts it, instantiates every Servlet inside it
// according to the exact lifecycle covered on the previous page, and the
// application is live at http://localhost:8080/myapp

Quick facts

  • Origin: Donated to the Apache Software Foundation in 1999, originally from Sun Microsystems — it became the reference implementation for the Servlet/JSP specifications
  • Written in: Java — runs anywhere a compatible JVM does
  • Default ports: 8080 (HTTP), 8443 (HTTPS), 8005 (shutdown listener)
  • Why it's the default: free, extensively documented, production-proven at massive scale, and the embedded server Spring Boot ships with out of the box

Version, Java baseline, and package namespace

Tomcat versionMinimum JavaServlet specPackage namespace
Tomcat 8.5Java 7+Servlet 3.1javax.servlet.*
Tomcat 9Java 8+Servlet 4.0javax.servlet.*
Tomcat 10Java 11+Servlet 5.0/6.0jakarta.servlet.*
Tomcat 11Java 17+Servlet 6.1jakarta.servlet.*
The javax/jakarta split, and why it matters here specifically

The mechanics and history of this migration are covered in full on Servlet Containers. The practical consequence for Tomcat specifically: if your application still imports javax.servlet.*, you need Tomcat 9 or earlier, or a full migration of your own code and every dependency to the jakarta namespace before it will run on Tomcat 10+.

Directory Structure

apache-tomcat-10.1.x/
├── bin/                    ← startup/shutdown scripts
│   ├── startup.sh / .bat  (start)
│   ├── shutdown.sh / .bat (stop)
│   ├── catalina.sh        (main script, more options — "catalina.sh run" for foreground output)
│   └── setenv.sh          (YOUR custom JVM options go here — never edit catalina.sh itself)
│
├── conf/                   ← configuration files
│   ├── server.xml         (main server configuration)
│   ├── context.xml        (default context/resource settings, shared by every app)
│   ├── tomcat-users.xml   (Manager app credentials)
│   └── logging.properties
│
├── lib/                   ← Tomcat's own JARs (servlet-api, jsp-api, etc.)
├── logs/                  ← catalina.out is the first place to look for any error
├── webapps/               ← YOUR APPLICATIONS GO HERE — drop a WAR, it auto-deploys
└── work/                  ← compiled JSPs, session data — safe to clear
  • webapps/: drop a WAR here for automatic deployment
  • conf/: everything configurable; server.xml is the file you'll edit most
  • logs/: the first place to check when anything goes wrong — catalina.out captures all console output

Installing and Running Tomcat

Download and extract

# From https://tomcat.apache.org/
tar -xzf apache-tomcat-10.1.x.tar.gz
cd apache-tomcat-10.1.x
chmod +x bin/*.sh
./bin/startup.sh          # or: ./bin/catalina.sh run  (foreground, output in terminal)
# http://localhost:8080 should now show the Tomcat welcome page
./bin/shutdown.sh

Package manager

# Ubuntu/Debian
sudo apt install tomcat10

# macOS (Homebrew)
brew install tomcat

Setting JVM options the right way

# bin/setenv.sh — create this file yourself; never edit catalina.sh directly,
# since that file is replaced on every Tomcat version upgrade
CATALINA_OPTS="$CATALINA_OPTS -Xms512m"
CATALINA_OPTS="$CATALINA_OPTS -Xmx2048m"
CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
CATALINA_OPTS="$CATALINA_OPTS -Dspring.profiles.active=production"
export CATALINA_OPTS

Key Configuration Files

server.xml — the main configuration

<!-- conf/server.xml -->
<Server port="8005" shutdown="SHUTDOWN">
    <Service name="Catalina">
        <Connector
            port="8080" protocol="HTTP/1.1"
            connectionTimeout="20000"
            maxThreads="200"
            redirectPort="8443" />

        <Engine name="Catalina" defaultHost="localhost">
            <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
                <Valve className="org.apache.catalina.valves.AccessLogValve"
                       directory="logs" pattern="%h %l %u %t "%r" %s %b" />
            </Host>
        </Engine>
    </Service>
</Server>

tomcat-users.xml — Manager authentication

<!-- conf/tomcat-users.xml -->
<tomcat-users>
    <role rolename="manager-gui"/>
    <role rolename="manager-script"/>
    <user username="deploy-bot"
          password="${MANAGER_PASSWORD}"
          roles="manager-script"/>
</tomcat-users>
Plaintext passwords here are only as safe as this file's own access controls — harden both

The password attribute above is a plaintext value by default — acceptable only because the file itself must already be filesystem-restricted to the user running Tomcat. Two independent layers of hardening matter for any Manager-enabled install: first, never grant manager-gui (the human web UI) to an automated deploy account — use the narrower manager-script role and restrict it to CI/CD's specific IP range in context.xml; second, configure a digesting CredentialHandler on the Realm (org.apache.catalina.realm.MessageDigestCredentialHandler with a modern algorithm) so the password stored on disk is never plaintext in the first place, exactly the same principle covered on Password Hashing. And in production, the Manager app should usually not be network-reachable at all outside a CI pipeline's specific runner.

context.xml — shared resources (this is where JNDI actually lives)

<!-- conf/context.xml — this is the concrete, Tomcat-specific home for the
     JNDI resource-binding pattern referenced on the "What is an Application
     Server?" page: the server configures the actual connection, application
     code only ever looks it up by name. -->
<Context>
    <Resource
        name="jdbc/shopDB"
        auth="Container"
        type="javax.sql.DataSource"
        driverClassName="org.postgresql.Driver"
        url="jdbc:postgresql://db.internal:5432/shop"
        username="${DB_USER}"
        password="${DB_PASSWORD}"
        maxTotal="100" maxIdle="30" maxWaitMillis="10000"/>
</Context>

Deploying Applications

Drop the WAR (simplest, fine for a single standalone instance)

cp myapp.war /opt/tomcat/webapps/
# Tomcat detects it, extracts to webapps/myapp/, deploys, live at /myapp
# Undeploy: remove BOTH the WAR and the extracted directory
rm /opt/tomcat/webapps/myapp.war
rm -rf /opt/tomcat/webapps/myapp

Manager web interface

# http://localhost:8080/manager/html — login with tomcat-users.xml credentials
# Upload a WAR, or start/stop/reload/undeploy an existing application
Dedicated Tomcat Maven plugins are largely legacy today

Older guides commonly reference tomcat7-maven-plugin for mvn tomcat7:deploy-style workflows — it hasn't kept pace with current Tomcat versions and sees little maintenance today. The more common modern paths are: build a container image (FROM tomcat:10.1-jdk21, COPY target/myapp.war /usr/local/tomcat/webapps/) and let Kubernetes or your orchestrator handle the actual deployment, or call the Manager's text interface directly from a CI pipeline via a plain HTTP request rather than a dedicated build-tool plugin. If you're on Spring Boot, none of this applies at all — the embedded server ships inside the JAR and "deployment" is just running it.

Context paths — the WAR filename determines the URL

webapps/ROOT.war       → http://localhost:8080/
webapps/api.war        → http://localhost:8080/api
webapps/v2#api.war     → http://localhost:8080/v2/api   // "#" encodes a nested path segment

Tomcat's Internal Architecture

This hierarchy maps directly onto the request lifecycle stages covered on the "What is an Application Server?" page — here's exactly where each stage lives inside Tomcat's own component model and configuration files.

ComponentPurposeConfigured in
ServerThe entire Tomcat instance — top-level container, exactly one per processserver.xml (root element)
ServiceGroups one or more Connectors with a single Engineserver.xml <Service>
ConnectorListens on a port, handles the network/HTTP protocol layer — a Service can have several (HTTP, HTTPS)server.xml <Connector>
EngineRoutes a parsed request to the correct virtual Hostserver.xml <Engine>
HostA virtual host — one server can answer for multiple domainsserver.xml <Host>
ContextOne deployed application — one WAR becomes one Contextcontext.xml, or META-INF/context.xml inside the WAR itself
Reading this top-down: Server → Service → Connector(s) + Engine → Host(s) → Context(s)

One Server runs one or more Services. Each Service pairs its Connector(s) — the actual network listeners — with exactly one Engine, which processes every parsed request and hands it to the correct Host based on the requested domain. Each Host can run several Contexts side by side — this is the mechanism that lets one Tomcat instance serve several independent applications at once, each with its own isolated classloader.

Virtual Threads in a Standalone Tomcat Install

The Virtual Threads capacity-planning shift covered on the "What is an Application Server?" page applies to Tomcat itself, not only to Spring Boot's embedded usage of it. On Java 21+, Tomcat 10.1+ can back its request-handling Executor with virtual threads instead of a bounded platform-thread pool.

// If Tomcat is embedded via Spring Boot 3.2+, this is the supported,
// well-documented path — covered in full on the previous pages:
spring.threads.virtual.enabled=true

// For a STANDALONE Tomcat instance (not Spring Boot embedded), the same
// capability is exposed through server.xml's Executor configuration on
// Tomcat 10.1.x+ running on Java 21+. The exact element and attribute names
// have evolved across recent point releases faster than most Tomcat
// configuration — check the release notes for the specific 10.1.x/11.x
// version you're running rather than copying a syntax from an older guide.
Same caveat as before: this helps blocking I/O wait, not CPU-bound work

Enabling a virtual-thread executor removes the platform-thread ceiling that turns downstream latency into an application-wide capacity wall — it does not make a slow database query or a CPU-heavy report generation any faster. Measure where your actual bottleneck is (thread-pool exhaustion with idle CPU points at blocking I/O; high CPU with a healthy thread pool points elsewhere) before treating this as a general performance lever.

Troubleshooting Common Issues

Tomcat won't start

lsof -i :8080                       # Linux/Mac — is the port already taken?
netstat -ano | findstr 8080         # Windows equivalent
tail -f logs/catalina.out           # the error is almost always right here
# Common causes: port conflict, JAVA_HOME unset, insufficient file permissions,
# a corrupted WAR that fails to extract

Application won't load

tail -f logs/localhost.YYYY-MM-DD.log
# Common causes: missing dependency in WEB-INF/lib, a malformed web.xml,
# a javax/jakarta package mismatch against this Tomcat version, or the
# application's own database connection failing on startup

Out of memory

# bin/setenv.sh
CATALINA_OPTS="-Xms1024m -Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat/logs/"
# The Manager app's "Find leaks" tool can help identify a leaking classloader
# after repeated hot redeploys — a common source in development environments

Slow under load with healthy CPU

# This is the classic thread-pool-exhaustion-while-idle signature covered
# in the Virtual Threads section above — check thread pool saturation first
<Connector port="8080" maxThreads="500" />   // a band-aid; diagnose the real bottleneck first

Production Best Practices

✅ Do

  • Remove or IP-restrict examples/, docs/, and the Manager app before going to production
  • Configure a digesting CredentialHandler for Manager credentials rather than relying on plaintext in tomcat-users.xml
  • Put all custom JVM options in bin/setenv.sh, never edit catalina.sh directly — it's replaced on every upgrade
  • Diagnose thread-pool exhaustion against actual CPU/I/O metrics before raising maxThreads as a reflex fix
  • Version-control your conf/ directory the same way you would application source code
  • Prefer a container image over a dedicated Maven Tomcat plugin for modern CI/CD pipelines

❌ Don't

  • Don't leave the Manager app reachable from the public internet, even with a strong password — restrict by network/IP as a second, independent layer
  • Don't assume raising maxThreads fixes a slow application — it often just delays the same downstream bottleneck by a bit more headroom
  • Don't mix a javax.servlet-based dependency into a Tomcat 10+ deployment and expect a clear compile-time error — it typically fails at runtime instead
  • Don't hardcode database credentials directly in context.xml — use environment variable substitution or a secrets manager

Interview Questions

🎓 Junior level

Q: What is Apache Tomcat, and how does it relate to the Servlet specification?
Tomcat is an open-source, Apache Software Foundation project that implements the Jakarta Servlet, JSP, and WebSocket specifications — it's the concrete runtime that actually executes code written against those specs. It also serves as the reference implementation the specifications themselves are measured against.

Q: What does the WAR filename determine when dropped into webapps/?
The context path — the URL prefix the application is served under. myapp.war becomes available at /myapp, while ROOT.war becomes the root application, served at /.

Q: Where should custom JVM options (heap size, GC flags) be configured, and why not directly in catalina.sh?
In a separate bin/setenv.sh file, which Tomcat's startup scripts source automatically if present. catalina.sh itself ships with Tomcat and gets overwritten on every version upgrade — any customization made directly to it is silently lost the next time Tomcat is updated.

🔥 Senior level

Q: One Tomcat instance hosts three separate applications, each with its own set of dependencies, some of which conflict on version. How does Tomcat's architecture make this safe?
Each deployed WAR becomes its own Context, and each Context gets its own classloader in Tomcat's classloader hierarchy — a web-application classloader that loads from that specific WEB-INF/lib and WEB-INF/classes, delegating only shared/common classes up to Tomcat's own classloader. Two applications can depend on conflicting versions of the same library without interfering, because each Context's classloader resolves that library from its own isolated WAR contents rather than a single shared classpath across the whole server instance.

Q: A production Tomcat instance shows thread-pool exhaustion (all maxThreads busy) while CPU utilization stays low. What's the most likely cause, and what are the two available remedies?
Threads are occupied waiting on something — almost certainly a slow downstream dependency (a database query, a call to another service) — not computing, which is why CPU stays low even as the pool saturates. Two genuinely different remedies exist: fix or bound the actual slow dependency (add an index, add a timeout, add a circuit breaker so slow calls fail fast instead of holding a thread indefinitely), or remove the platform-thread ceiling itself by adopting Virtual Threads on Java 21+, so a blocked request no longer occupies a scarce pooled resource while it waits. Raising maxThreads alone treats neither cause — it only raises the number of concurrently blocked requests the pool can absorb before the same underlying problem resurfaces at a higher concurrency level.

Q: Why is context.xml's JNDI <Resource> pattern rarely used in a Spring Boot application, even one embedding Tomcat?
JNDI resource binding solves a specific problem of the external-server deployment model: the same WAR, deployed unmodified to different environments, needs each environment's server to supply environment-specific configuration (a different database URL in dev vs. prod) without the application code changing. A Spring Boot application embeds its own server and is built as a single self-contained artifact per environment (or configured via externalized properties and Spring profiles at startup) — there's no separately-administered external server to register a JNDI binding with in the first place, so the problem JNDI solves doesn't arise the same way. The equivalent goal — same artifact, environment-specific configuration — is achieved via application.yml profiles and environment variables instead.