Context & Deployment

The isolation boundary a running application actually lives inside — classloader mechanics, deployment strategies, and the memory leak that a missing contextDestroyed() causes

← Back to Index

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
    }
}
ScopeObjectLifetimeUse for
ApplicationServletContextEntire Context lifetimeConfiguration, shared caches, resources every request can see
SessionHttpSessionOne user's sessionLogin state, shopping cart — see Session Management
RequestHttpServletRequestSingle requestForm 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
Dedicated Tomcat Maven plugins are legacy — see WAR vs JAR Files

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>
Never reloadable="true" in production

It 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.
If you're on embedded Spring Boot + Kubernetes, you already have this covered elsewhere

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.
Why an undeployed Context's classloader sometimes never gets garbage collected

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-path in application.yml over 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

🎓 Junior level

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.

🔥 Senior level

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.