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
- Tomcat Directory Layout — CATALINA_HOME vs CATALINA_BASE
- Deploying WAR and webapps
- Optional: Linux Docker introduction
Docker: Official Tomcat Image
Quick run (smoke test):
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 21CATALINA_HOMEinside container:/usr/local/tomcat- Default
webapps/includes sample apps—replace for production
Stop and remove:
docker stop tomcat && docker rm tomcatMount WAR and Config (Persist Data)
Production-style compose fragment:
# 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:
cp target/api.war ./webapps/
docker compose up -d
curl http://127.0.0.1:8080/api/helloCode explanation:
- Bind
127.0.0.1:8080:8080— not public; pair with host Nginx - Mount
server.xmlread-only when you customize Connectors logs/on host fortailwithoutdocker 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)
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:
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-tomcatCode 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
| Approach | Pros | Cons |
|---|---|---|
| systemd + tar.gz Tomcat | Simple on one VPS, familiar logs | Manual JDK/Tomcat upgrades |
| Docker Tomcat | Reproducible image, CI builds | Extra 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:
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:
<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:
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.shCode explanation:
- Scripts always use
CATALINA_HOME/bin - Each instance has isolated
webapps/andlogs/ - Nginx upstream can load-balance 8080 and 8081 (preview):
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:
# /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=tomcatDuplicate for inst2 with CATALINA_BASE=/var/tomcat/inst2.
Backup Before Upgrade
Before bumping Tomcat 10.1.x → 10.1.y or 9 → 10:
| Backup | Path |
|---|---|
| Config | conf/ or /etc/tomcat10/ |
| WARs / docBase | webapps/, /opt/apps/*.war |
| Custom libs | lib/*.jar you added (JDBC drivers) |
| Instance data | Each CATALINA_BASE |
Upgrade product:
$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.shNever overwrite webapps/ and conf/ blindly with a fresh tarball.
Tomcat 9 → 10: Jakarta Migration
| Tomcat 9 | Tomcat 10 |
|---|---|
javax.servlet.* | jakarta.servlet.* |
| Spring Boot 2.x typical | Spring 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:9→tomcat: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:
- inst2 gets new WAR; health-check
8081 - Nginx upstream marks inst1 drain / removes from pool
- Restart inst1 with new WAR; add back to pool
- 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
| Chapter | Topic |
|---|---|
| 16–19 — Projects | Hands-on deploy labs |
| 20 — Troubleshooting | Docker and multi-instance issues |
| 18 — Nginx + Tomcat project | Full 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.