Tomcat, Docker, and Multi-Instance

Introduction

Beyond a single /opt/tomcat on a VPS, teams run Tomcat in Docker for reproducible environments and use multiple instances from one install via CATALINA_BASE. This chapter covers the official Tomcat image, volume mounts for WARs and config, a multi-instance directory layout, backup/upgrade habits, and Tomcat 9 → 10 Jakarta migration notes—operations topics that complement Installing Tomcat on bare metal.

Prerequisites

Docker: Official Tomcat Image

Quick run (smoke test):

bash
docker run -d --name tomcat \
  -p 8080:8080 \
  tomcat:10.1-jdk21-jammy
curl -I http://127.0.0.1:8080/

Code explanation:

  • tomcat:10.1-jdk21-jammy — Tomcat 10.1 on Ubuntu Jammy with JDK 21
  • CATALINA_HOME inside container: /usr/local/tomcat
  • Default webapps/ includes sample apps—replace for production

Stop and remove:

bash
docker stop tomcat && docker rm tomcat

Mount WAR and Config (Persist Data)

Production-style compose fragment:

yaml
# docker-compose.yml
services:
  tomcat:
    image: tomcat:10.1-jdk21-jammy
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - ./webapps:/usr/local/tomcat/webapps
      - ./conf/server.xml:/usr/local/tomcat/conf/server.xml:ro
      - ./logs:/usr/local/tomcat/logs
    environment:
      CATALINA_OPTS: "-Xms512m -Xmx1024m"

Deploy WAR:

bash
cp target/api.war ./webapps/
docker compose up -d
curl http://127.0.0.1:8080/api/hello

Code explanation:

  • Bind 127.0.0.1:8080:8080 — not public; pair with host Nginx
  • Mount server.xml read-only when you customize Connectors
  • logs/ on host for tail without docker exec

Warning

Do Not Mount Over bin/ or lib/ Blindly

Mount webapps/, conf/, logs/ selectively. Replacing entire /usr/local/tomcat breaks the image layout unless you know the full tree.

Custom Dockerfile (Add WAR at Build)

dockerfile
FROM tomcat:10.1-jdk21-jammy
 
RUN rm -rf /usr/local/tomcat/webapps/*
 
COPY target/api.war /usr/local/tomcat/webapps/api.war
 
EXPOSE 8080
CMD ["catalina.sh", "run"]

Build and run:

bash
mvn -f ../api/pom.xml clean package
docker build -t my-api-tomcat .
docker run -d -p 127.0.0.1:8080:8080 my-api-tomcat

Code explanation:

  • catalina.sh run — foreground process (PID 1 friendly with Docker)
  • Removing default webapps/* avoids shipping examples in prod images

Docker vs systemd on VPS

ApproachProsCons
systemd + tar.gz TomcatSimple on one VPS, familiar logsManual JDK/Tomcat upgrades
Docker TomcatReproducible image, CI buildsExtra layer, volume planning

Many Hello Code VPS guides use Nginx + systemd JAR or Nginx + Docker—pick one stack per environment.

Multi-Instance with CATALINA_BASE

One Tomcat product (CATALINA_HOME, shared bin/ and lib/), several instances with separate ports and webapps/:

Create instance 1:

bash
export CATALINA_HOME=/opt/tomcat
export CATALINA_BASE=/var/tomcat/inst1
 
mkdir -p $CATALINA_BASE/{conf,webapps,logs,temp,work}
cp -r $CATALINA_HOME/conf/* $CATALINA_BASE/conf/

Edit $CATALINA_BASE/conf/server.xml — unique HTTP port:

xml
<Connector port="8080" protocol="HTTP/1.1" address="127.0.0.1" />
<Server port="8005" shutdown="SHUTDOWN">

Instance 2: ports 8081 and shutdown 8006 (must be unique per instance).

Start:

bash
CATALINA_HOME=/opt/tomcat CATALINA_BASE=/var/tomcat/inst1 \
  /opt/tomcat/bin/startup.sh
 
CATALINA_HOME=/opt/tomcat CATALINA_BASE=/var/tomcat/inst2 \
  /opt/tomcat/bin/startup.sh

Code explanation:

  • Scripts always use CATALINA_HOME/bin
  • Each instance has isolated webapps/ and logs/
  • Nginx upstream can load-balance 8080 and 8081 (preview):
nginx
upstream tomcat_cluster {
    server 127.0.0.1:8080;
    server 127.0.0.1:8081;
}
location / {
    proxy_pass http://tomcat_cluster;
    include /etc/nginx/proxy_params;
}

Full load balancing: Nginx upstream.

systemd for Multi-Instance

Two units differing only in CATALINA_BASE and ExecStart env:

ini
# /etc/systemd/system/tomcat-inst1.service
[Service]
Environment=CATALINA_HOME=/opt/tomcat
Environment=CATALINA_BASE=/var/tomcat/inst1
Environment=CATALINA_OPTS=-Xms512m -Xmx1024m
ExecStart=/opt/tomcat/bin/catalina.sh run
User=tomcat

Duplicate for inst2 with CATALINA_BASE=/var/tomcat/inst2.

Backup Before Upgrade

Before bumping Tomcat 10.1.x → 10.1.y or 9 → 10:

BackupPath
Configconf/ or /etc/tomcat10/
WARs / docBasewebapps/, /opt/apps/*.war
Custom libslib/*.jar you added (JDBC drivers)
Instance dataEach CATALINA_BASE

Upgrade product:

bash
$CATALINA_HOME/bin/shutdown.sh
tar xzf apache-tomcat-10.1.NEW.tar.gz -C /opt
# point symlink /opt/tomcat to new version
# restore conf from backup; diff default server.xml for new defaults
$CATALINA_HOME/bin/startup.sh

Never overwrite webapps/ and conf/ blindly with a fresh tarball.

Tomcat 9 → 10: Jakarta Migration

Tomcat 9Tomcat 10
javax.servlet.*jakarta.servlet.*
Spring Boot 2.x typicalSpring Boot 3

Migration checklist:

  • Upgrade libraries to Jakarta artifacts
  • Rebuild WAR with jakarta.servlet-api
  • Re-test all endpoints and filters
  • Update Docker tag tomcat:9tomcat:10.1

Run 9 and 10 on different ports during parallel testing—not the same webapps/ with mixed APIs.

Rolling Deploy (Concept)

Zero-downtime on one machine:

  1. inst2 gets new WAR; health-check 8081
  2. Nginx upstream marks inst1 drain / removes from pool
  3. Restart inst1 with new WAR; add back to pool
  4. Repeat for inst2

Requires health endpoints and coordinated Nginx—advanced; single VPS learning can use brief maintenance window + systemctl restart tomcat.

Post-Chapter Checklist

  • Run Tomcat container with WAR volume or Dockerfile COPY
  • Explain CATALINA_HOME vs CATALINA_BASE
  • Know unique Connector and shutdown ports per instance
  • Backup conf and WARs before Tomcat version upgrade

What Comes Next

ChapterTopic
16–19 — ProjectsHands-on deploy labs
20 — TroubleshootingDocker and multi-instance issues
18 — Nginx + Tomcat projectFull stack

FAQ

Docker Tomcat as root?

Official image often runs as tomcat user in newer tags—check docker inspect. Avoid --user root in production.

Kubernetes?

Similar to Docker—Deployment + Service; ingress replaces Nginx on VM. Out of scope for this chapter.

Same WAR on two instances?

Yes—shared artifact, two CATALINA_BASE/webapps/ copies or shared read-only docBase (care with session affinity).

CATALINA_TMPDIR?

Set when temp/ should live on large tmpfs—Environment=CATALINA_TMPDIR=/var/tomcat/tmp in systemd.

webapps mount empty?

Tomcat deploys nothing—404 on /. Copy WAR into mounted webapps/.

Compose vs Kubernetes for Hello Code?

Compose on single VPS is enough for learning; K8s when you need cluster scheduling.