Virtual Hosts and Web Application Deployment
Tomcat supports multiple virtual hosts through Host configuration and can deploy WAR files or directories automatically or manually. This chapter covers the most common deployment patterns and safe operational practices.
Deployment Methods
Tomcat offers three deployment methods.
| Method | How | Characteristics |
|---|---|---|
| Auto Deploy | Copy WAR to webapps/ | Simple, not recommended for production |
| Manual Deploy | Create Context XML file | Explicit, recommended |
| Manager App | HTTP PUT or Web UI | CI/CD integration possible |
WAR File Auto Deployment
With autoDeploy="true" set on a Host, copying a WAR file to the webapps/ directory triggers automatic deployment.
# Copy WAR file to deployment directory
sudo cp myapp-1.2.0.war /opt/tomcat/latest/webapps/myapp.war
# Tomcat automatically extracts it
# /opt/tomcat/latest/webapps/myapp/ ← created
# Access URL: http://host:8080/myapp/
Auto Deployment Settings (server.xml)
<Host name="localhost" appBase="webapps"
unpackWARs="true"
autoDeploy="true"
deployOnStartup="true">
| Attribute | Description | Production Recommendation |
|---|---|---|
unpackWARs | Auto-extract WAR | true |
autoDeploy | Auto-detect/deploy WARs at runtime | false |
deployOnStartup | Scan appBase on startup | true |
Production warning:
autoDeploy="true"causes Tomcat to periodically scan the filesystem. An incompletely copied WAR can be deployed mid-copy, soautoDeploy="false"is recommended for production.
Manual Deploy via Context XML (Recommended)
Create Context XML files in conf/Catalina/localhost/ for explicit deployment path control.
$CATALINA_HOME/conf/
└── Catalina/
└── localhost/
├── ROOT.xml → / (root context)
├── myapp.xml → /myapp
└── api.xml → /api
Basic Context XML
<!-- conf/Catalina/localhost/myapp.xml -->
<Context docBase="/var/www/myapp"
path="/myapp"
reloadable="false"
crossContext="false"
privileged="false">
</Context>
With JNDI DataSource
<!-- conf/Catalina/localhost/myapp.xml -->
<Context docBase="/var/www/myapp"
path="/myapp"
reloadable="false">
<Resource name="jdbc/myDB"
auth="Container"
type="javax.sql.DataSource"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://db-host:3306/mydb?useSSL=false&serverTimezone=Asia/Seoul"
username="appuser"
password="AppP@ssw0rd"
maxTotal="50"
maxIdle="10"
minIdle="5"
maxWaitMillis="30000"
validationQuery="SELECT 1"
testOnBorrow="true"
removeAbandoned="true"
removeAbandonedTimeout="60"
logAbandoned="true"/>
</Context>
Replacing the ROOT Context (Deploy at Root Path)
<!-- conf/Catalina/localhost/ROOT.xml -->
<Context docBase="/var/www/myapp/current"
reloadable="false">
</Context>
This serves the app in /var/www/myapp/current at http://host:8080/.
Multiple Virtual Host Configuration
server.xml
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="false">
</Host>
<Host name="app1.example.com" appBase="/var/www/app1"
unpackWARs="true" autoDeploy="false">
<Alias>www.app1.example.com</Alias>
<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs"
prefix="app1_access_log"
suffix=".txt"
pattern="%h %l %u %t "%r" %s %b %D"/>
</Host>
<Host name="app2.example.com" appBase="/var/www/app2"
unpackWARs="true" autoDeploy="false">
</Host>
</Engine>
Context File Structure per Virtual Host
conf/
└── Catalina/
├── localhost/
│ └── ROOT.xml
├── app1.example.com/
│ └── ROOT.xml → root context for app1
└── app2.example.com/
└── ROOT.xml → root context for app2
Deployment via Tomcat Manager
Manager App Account Setup
<!-- conf/tomcat-users.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users>
<role rolename="manager-gui"/>
<role rolename="manager-script"/>
<role rolename="admin-gui"/>
<user username="admin"
password="MyStr0ngP@ssw0rd!"
roles="manager-gui,manager-script,admin-gui"/>
</tomcat-users>
Restrict Manager Access by IP
<!-- webapps/manager/META-INF/context.xml -->
<Context antiResourceLocking="false" privileged="true">
<CookieProcessor className="org.apache.tomcat.util.http.Rfc6265CookieProcessor"
sameSiteCookies="strict"/>
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="127\.0\.0\.1|192\.168\.1\.\d+"/>
</Context>
Remote Deployment via curl (CI/CD)
# Deploy WAR file
curl -u admin:MyStr0ngP@ssw0rd! \
"http://localhost:8080/manager/text/deploy?path=/myapp&update=true" \
--upload-file /path/to/myapp.war
# List deployed applications
curl -u admin:MyStr0ngP@ssw0rd! \
"http://localhost:8080/manager/text/list"
# Undeploy application
curl -u admin:MyStr0ngP@ssw0rd! \
"http://localhost:8080/manager/text/undeploy?path=/myapp"
# Restart
curl -u admin:MyStr0ngP@ssw0rd! \
"http://localhost:8080/manager/text/restart?path=/myapp"
Zero-Downtime Deployment Patterns
Pattern 1: WAR Replacement + Restart
#!/bin/bash
set -e
CATALINA_HOME=/opt/tomcat/latest
APP_NAME=myapp
WAR_FILE=/tmp/myapp-1.2.0.war
# 1. Backup previous WAR
if [ -f "$CATALINA_HOME/webapps/${APP_NAME}.war" ]; then
cp "$CATALINA_HOME/webapps/${APP_NAME}.war" \
"/var/backup/${APP_NAME}-$(date +%Y%m%d-%H%M%S).war"
fi
# 2. Remove old deployment (prevent stale classes)
rm -rf "$CATALINA_HOME/webapps/${APP_NAME}"
rm -f "$CATALINA_HOME/webapps/${APP_NAME}.war"
rm -rf "$CATALINA_HOME/work/Catalina/localhost/${APP_NAME}"
# 3. Copy new WAR
cp "$WAR_FILE" "$CATALINA_HOME/webapps/${APP_NAME}.war"
# 4. Restart Tomcat
sudo systemctl restart tomcat
echo "Deployment complete: ${APP_NAME}"
Pattern 2: Blue-Green Deployment (with Nginx)
#!/bin/bash
# Blue-Green deployment (Nginx + 2 Tomcat instances)
NEW_WAR=/tmp/myapp-1.2.0.war
ACTIVE=$(cat /var/run/active-tomcat)
if [ "$ACTIVE" = "blue" ]; then
TARGET_PORT=8081
NEW_ACTIVE="green"
else
TARGET_PORT=8080
NEW_ACTIVE="blue"
fi
# Deploy to inactive instance
cp "$NEW_WAR" "/opt/tomcat-${NEW_ACTIVE}/webapps/ROOT.war"
systemctl restart "tomcat-${NEW_ACTIVE}"
# Health check
sleep 10
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
http://localhost:${TARGET_PORT}/actuator/health)
if [ "$HTTP_STATUS" = "200" ]; then
# Switch Nginx
sed -i "s/proxy_pass http:\/\/localhost:[0-9]*/proxy_pass http:\/\/localhost:${TARGET_PORT}/" \
/etc/nginx/sites-available/myapp.conf
nginx -s reload
echo "$NEW_ACTIVE" > /var/run/active-tomcat
echo "✅ Deployment successful: ${NEW_ACTIVE} (port ${TARGET_PORT})"
else
echo "❌ Health check failed — rolling back"
exit 1
fi
Post-Deployment Verification
# Check deployment status in Tomcat logs
tail -100 /opt/tomcat/latest/logs/catalina.out | grep -E "(deploy|error|warn)" -i
# Successful deployment message
# INFO [main] org.apache.catalina.startup.HostConfig.deployWAR
# Deployment of web application archive [.../myapp.war] has finished in X ms
# Access test
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/myapp/
# 200
# List deployed apps via Manager API
curl -u admin:password http://localhost:8080/manager/text/list
Summary
| Item | Details |
|---|---|
| Default deploy path | $CATALINA_HOME/webapps/ |
| Recommended deploy method | Context XML file + autoDeploy="false" |
| ROOT context | conf/Catalina/localhost/ROOT.xml |
| Manager security | Strong password + IP restriction |
| CI/CD integration | Manager REST API (via curl) |
| Zero-downtime | Blue-Green pattern + Nginx port switching |