What is a Context — and Why Does Deployment Need One?
Every earlier page in this topic described a piece of the machinery —
the container, the archive format, the XML configuration. A
Context is where all of it becomes one running,
addressable, isolated application: it's the container's runtime
representation of a single deployed WAR, with its own URL namespace, its
own classloader, and its own ServletContext object. Without
a Context, a compiled application is just bytes sitting in a directory —
nothing gives it a URL, nothing isolates its classes from another
application's, and nothing manages its start/stop lifecycle
independently of the rest of the server.
// BEFORE deployment — compiled classes and a WAR file, addressable by nothing
target/myapp.war
// This file has no URL, no running classloader, no lifecycle. It's inert.
// AFTER deployment — the container creates a Context from it
$ cp target/myapp.war /opt/tomcat/webapps/
// The container now maintains, for this one application specifically:
// - a context path: /myapp
// - an isolated classloader: WEB-INF/classes and WEB-INF/lib resolve
// here first, independent of any other deployed application
// - a live ServletContext object your code can query at runtime
// - a start/stop/reload lifecycle independent of every other Context
// http://localhost:8080/myapp/products/list is now a real, addressable request
Reading the context path out of a URL
http://localhost:8080/myapp/products/list
└────┬────┘└──┬──┘└──────┬──────┘
Host:Port Context Path within
Path the application
# myapp.war → context path /myapp (the WAR's filename determines this by default)
The ServletContext Object
public class CatalogServlet extends HttpServlet {
@Override
public void init() throws ServletException {
ServletContext ctx = getServletContext();
String adminEmail = ctx.getInitParameter("admin.email"); // from web.xml <context-param>
ctx.setAttribute("catalogCache", new CatalogCache()); // shared across every servlet in this Context
String uploadDir = ctx.getRealPath("/uploads"); // resolves to an actual filesystem path
ctx.log("CatalogServlet initialized"); // writes to this Context's own log
}
}
| Scope | Object | Lifetime | Use for |
|---|---|---|---|
| Application | ServletContext | Entire Context lifetime | Configuration, shared caches, resources every request can see |
| Session | HttpSession | One user's session | Login state, shopping cart — see Session Management |
| Request | HttpServletRequest | Single request | Form data, request-scoped parameters |
Deployment Methods
WAR drop (most common for a single standalone instance)
cp myapp.war $CATALINA_HOME/webapps/
# Detected, extracted to webapps/myapp/, deployed, live at /myapp
Exploded directory (development convenience)
webapps/myapp/
├── WEB-INF/{web.xml, classes/, lib/}
├── index.html
└── css/
# Edit files directly without rebuilding the WAR — faster iteration locally,
# not a pattern to rely on for any real deployment
Context XML file (per-application configuration, outside web.xml)
<!-- $CATALINA_HOME/conf/Catalina/localhost/myapp.xml -->
<Context docBase="/path/to/myapp.war" reloadable="false">
<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"/>
<Environment name="maxUploadSize" value="10485760" type="java.lang.Integer"/>
</Context>
Tomcat Manager
# http://localhost:8080/manager/html — GUI deployment
# Or the text interface, scriptable from CI:
curl -u deploy-bot:${MANAGER_PASSWORD} \
-T myapp.war \
"http://localhost:8080/manager/text/deploy?path=/myapp&update=true"
# Credential hardening for this account is covered on Tomcat Overview —
# narrow role, digest-hashed password, IP-restricted access
Older guides reference tomcat7-maven-plugin
(mvn tomcat7:deploy) for this exact workflow. It's seen
little maintenance in recent years; a container image built from the
extracted WAR, or a direct call to the Manager's text interface from
CI as shown above, are the more current equivalents.
Looking Up JNDI Resources
The <Resource> declared in the Context XML above is
exactly the mechanism referenced on What is
an Application Server? — the server owns the actual connection
details, and application code looks the resource up by name rather than
hardcoding them.
public class CatalogServlet extends HttpServlet {
private DataSource dataSource;
@Override
public void init() throws ServletException {
try {
Context initCtx = new InitialContext();
Context envCtx = (Context) initCtx.lookup("java:comp/env");
dataSource = (DataSource) envCtx.lookup("jdbc/shopDB");
} catch (NamingException e) {
throw new ServletException("Cannot find DataSource", e);
}
}
}
Hot Deployment
<!-- context.xml or server.xml Host element -->
<Context reloadable="true"> <!-- watches WEB-INF/classes and WEB-INF/lib for changes -->
</Context>
reloadable="true" in productionIt causes exactly the classloader-leak risk explained in full later on this page (an old classloader can't be garbage collected while anything still references it), adds constant CPU overhead from watching the filesystem, and makes reload timing unpredictable under real traffic. It exists purely for local development convenience.
Production Deployment Strategies
Stop → deploy → start (simple, brief downtime)
curl "http://localhost:8080/manager/text/stop?path=/myapp"
curl -T myapp.war "http://localhost:8080/manager/text/deploy?path=/myapp&update=true"
curl "http://localhost:8080/manager/text/start?path=/myapp"
Parallel deployment (zero downtime, Tomcat-specific)
# Version-in-filename: appname##version.war
myapp##001.war → the currently active version
myapp##002.war → the new version, deployed alongside it
# Existing sessions continue on their original version; new sessions go to
# the newest one; the old version undeploys automatically once its last
# session ends — no request is ever dropped mid-flight.
Blue-green deployment (two full environments, instant rollback)
# BLUE (current) stays serving traffic while GREEN (new version) is deployed
# and tested independently. A load balancer switch moves traffic to GREEN;
# BLUE stays warm and ready for an instant rollback if GREEN misbehaves.
The three strategies above are specific to a traditional,
externally-hosted Tomcat instance managing WAR deployments directly.
An embedded Spring Boot application running in Kubernetes achieves
the same zero-downtime goal through a completely different, already
cross-referenced mechanism: rolling pod replacement, graceful
shutdown (server.shutdown=graceful), and a readiness
probe — covered in full on What is an
Application Server?. Which model applies depends entirely on
which deployment topology (external server vs. embedded container)
your application uses.
Classloading and Isolation — and the Memory Leak That Follows From It
Bootstrap ClassLoader
└── System ClassLoader
└── Common ClassLoader ($CATALINA_HOME/lib — shared across ALL apps)
├── WebApp1 ClassLoader (WEB-INF/classes, WEB-INF/lib)
├── WebApp2 ClassLoader (WEB-INF/classes, WEB-INF/lib)
└── WebApp3 ClassLoader (WEB-INF/classes, WEB-INF/lib)
// Each Context gets its OWN classloader, checked before delegating up to
// the shared Common one. Two apps can depend on conflicting versions of the
// same library without interfering — this is the mechanism referenced on
// Tomcat Overview for exactly this scenario.
Undeploying a Context should let its entire classloader — and every
class and static field loaded through it — become eligible for
garbage collection. It doesn't, if anything outside that
Context still holds a reference into it: a JDBC driver registered in
DriverManager's JVM-wide static list, a non-daemon
thread the application started and never stopped, or a
ThreadLocal value left set on a thread that belongs to
the container's shared thread pool rather than to this Context
alone. Any one of these keeps a live reference chain back to the old
classloader, and the JVM cannot collect a classloader while anything
still points to it — repeated hot redeploys compound this until
Metaspace is exhausted. This is exactly the same root cause behind
the static Set<Session> leak described on
WebSockets: something
outside the Context's own lifecycle outlived the Context itself.
@WebListener
public class CleanupListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent event) {
executorService.shutdown(); // stop any thread this Context started
// Deregister JDBC drivers THIS context registered — DriverManager's list
// is JVM-wide, shared across every deployed Context, so a driver
// registered here but never deregistered outlives this Context entirely
Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
Driver driver = drivers.nextElement();
try {
DriverManager.deregisterDriver(driver);
} catch (SQLException e) {
log.warn("Error deregistering driver", e);
}
}
connectionPool.close(); // release pooled connections rather than leaving them open
}
}
Spring Boot Context Configuration
# application.yml
server:
port: 8080
servlet:
context-path: /shop
session:
timeout: 30m
cookie:
http-only: true
secure: true
tomcat:
threads:
max: 200
min-spare: 10
Session cookie hardening here follows the exact same reasoning as
Session Management; thread
pool sizing and when to prefer Virtual Threads instead of raising
server.tomcat.threads.max is covered in depth on
What is an Application Server?.
Best Practices and Common Pitfalls
✅ Do
- Release, in
contextDestroyed(), everything a Context's own code acquired — threads, JDBC driver registrations, connection pools - Use environment variable substitution for credentials in Context XML resources, never hardcoded plaintext
- Choose a deployment strategy that matches your actual topology — parallel/blue-green for an external Tomcat, rolling + graceful shutdown for embedded Spring Boot in Kubernetes
- Keep
reloadable="true"strictly to local development - Prefer
server.servlet.context-pathinapplication.ymlover any XML-based context configuration for a Spring Boot application
❌ Don't
- Don't leave a background thread or a registered JDBC driver unreleased when a Context shuts down — it silently prevents that Context's entire classloader from ever being collected
- Don't enable
reloadable="true"anywhere near production traffic - Don't assume a Tomcat-specific deployment strategy (parallel deployment, Manager-driven stop/start) applies to an embedded Spring Boot app in Kubernetes — the mechanisms are different and covered separately
- Don't hardcode database credentials in a Context XML
<Resource>— use environment variable placeholders or a secrets manager
Interview Questions
Q: What is a Context, and what does it isolate?
A Context is the container's runtime representation of one deployed web
application — it has its own URL context path, its own classloader
(isolating it from other deployed applications' classes), and its own
ServletContext object, and can be started, stopped, or
reloaded independently of every other deployed application.
Q: How does a WAR's filename determine its context path?
By default, the filename (minus the .war extension) becomes
the context path. myapp.war deploys at /myapp;
ROOT.war deploys at the root path /.
Q: Why is reloadable="true" discouraged in production?
It makes Tomcat continuously watch WEB-INF/classes and
WEB-INF/lib for file changes, adding constant CPU overhead,
and it risks classloader leaks from repeated reloads — real production
deployments use one of the explicit strategies (stop/deploy/start,
parallel deployment, blue-green) instead.
Q: An application has been hot-redeployed dozens of times in a long-running staging environment, and Metaspace usage keeps climbing until an OutOfMemoryError. The application's own heap looks fine. Diagnose the likely cause.
This is the signature of a classloader leak: each hot redeploy should
make the previous Context's entire classloader — and every class loaded
through it — eligible for garbage collection, but something outside that
Context's own lifecycle still holds a live reference into it. The most
common culprits are a JDBC driver registered in
DriverManager's JVM-wide static list and never
deregistered, a non-daemon thread the application started that outlives
the Context, or a ThreadLocal value set on a thread from the
container's shared pool and never cleared. Since Metaspace (where
classloader metadata itself lives) climbs rather than the object heap,
the fix is a ServletContextListener.contextDestroyed() that
symmetrically releases everything contextInitialized()
acquired — a heap dump analysis showing multiple retained
WebappClassLoader instances confirms the diagnosis directly.
Q: Why does parallel deployment (myapp##001.war / myapp##002.war) achieve zero downtime without a load balancer, while blue-green deployment requires one?
Parallel deployment relies on the Context mechanism directly: Tomcat
treats each version suffix as a distinct Context sharing the same
context path, routing existing sessions to whichever version they
started on while sending new sessions to the newest deployed version,
within a single Tomcat instance. Blue-green deployment operates one level
up the stack — two entirely separate environments (potentially separate
server instances or containers), with traffic routing controlled
externally by a load balancer or reverse proxy switch. Parallel
deployment is Tomcat's own session-aware mechanism for a single instance;
blue-green is an infrastructure-level pattern that works identically
regardless of what's actually running inside either environment.
Q: A team asks whether Tomcat's parallel deployment feature will help them achieve zero-downtime deploys for their Spring Boot application running in Kubernetes. What do you tell them?
Parallel deployment is a feature of Tomcat's own Host/Context management
of multiple WAR versions inside a single Tomcat instance — it has no
equivalent meaning for an embedded Spring Boot application, where each
Kubernetes pod runs exactly one instance of exactly one version, and
"multiple versions in one Context" isn't how the embedded model works at
all. The correct mechanism for that topology is Kubernetes' own rolling
deployment: new pods with the new version start and pass their readiness
probe before old pods are terminated, and server.shutdown=graceful
ensures a terminating pod finishes in-flight requests rather than dropping
them — an entirely different mechanism achieving the same zero-downtime
goal at a different layer of the stack.