WAR deployment demonstration in Tomcat



When an application slows down or starts throwing "cannot get a connection" errors, the cause is very often the connection pool between the app server and the database. A pool that is too small queues requests. One that is too big floods the database with sessions. One without validation hands out dead connections after a database restart.

In this guide I install Apache Tomcat 9 on Linux, connect it to Oracle 19c through a JNDI datasource, and explain every pool setting from a DBA's point of view. Then I deploy a WAR and turn Tomcat into a proper systemd service.

ComponentUsed here
OSRHEL / Oracle Linux 8 or 9
Tomcat9.0.x (latest patch release)
JavaOpenJDK 11 (Java 8 also works)
DatabaseOracle 19c, service ORCLPDB1
JDBC driverojdbc8.jar
Install path/opt/tomcat/latest (= $CATALINA_HOME)

STEP 1
Create a service user and install Java

Run Tomcat as its own unprivileged user, not as root and not as the oracle database owner. Tomcat 9 needs Java 8 or later.

sudo useradd -r -m -d /opt/tomcat -s /bin/bash tomcat
sudo dnf install -y java-11-openjdk-headless      # or java-1.8.0-openjdk-headless
java -version

STEP 2
Download and unpack Tomcat 9

Download the latest 9.0.x "Core" tar.gz from tomcat.apache.org. A newer patch release always includes security fixes, so do not pin an old version. In the commands below, replace 9.0.x with the version you downloaded.

cd /opt/tomcat
sudo -u tomcat tar -xzf /tmp/apache-tomcat-9.0.x.tar.gz
sudo -u tomcat ln -s /opt/tomcat/apache-tomcat-9.0.x /opt/tomcat/latest

export CATALINA_HOME=/opt/tomcat/latest
ls $CATALINA_HOME
# bin  conf  lib  logs  temp  webapps  work

The latest symlink lets you upgrade later by unpacking a new version and moving the link.

STEP 3
Set Java and memory in setenv.sh

Tomcat reads bin/setenv.sh at startup if it exists. This is the right place for your settings: it is never overwritten when you upgrade.

$CATALINA_HOME/bin/setenv.sh
export JAVA_HOME=/usr/lib/jvm/jre-11-openjdk
export CATALINA_OPTS="-Xms1g -Xmx2g -server -Djava.security.egd=file:/dev/urandom"
export CATALINA_PID="$CATALINA_HOME/temp/tomcat.pid"
chmod 750 $CATALINA_HOME/bin/setenv.sh

STEP 4
Add the Oracle JDBC driver

Copy ojdbc8.jar (for Java 8 and 11) from the 19c Oracle Home into Tomcat's lib folder. The datasource is created by Tomcat itself, so the driver must be in Tomcat's classpath, not inside the WAR.

scp oracle@dbhost:/u01/app/oracle/product/19.0.0/dbhome_1/jdbc/lib/ojdbc8.jar /tmp/
sudo -u tomcat cp /tmp/ojdbc8.jar $CATALINA_HOME/lib/

STEP 5
Configure the Oracle datasource

There are two places to define a JNDI datasource in Tomcat:

WhereScopeWhen to use
conf/context.xmlShared by every application on this TomcatSimplest: one app, or several apps on the same schema
conf/server.xml <GlobalNamingResources>Global; each app must add a <ResourceLink>Many apps that must share one pool

For most setups, context.xml is the simpler choice. Add this inside <Context>:

$CATALINA_HOME/conf/context.xml
<Context>
    ...
    <Resource name="jdbc/AppDS"
              auth="Container"
              type="javax.sql.DataSource"
              factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
              driverClassName="oracle.jdbc.OracleDriver"
              url="jdbc:oracle:thin:@//dbhost:1521/ORCLPDB1"
              username="app_user"
              password="App#Pass2026"

              initialSize="10"
              minIdle="10"
              maxIdle="50"
              maxActive="100"
              maxWait="10000"

              validationQuery="SELECT 1 FROM DUAL"
              testOnBorrow="true"
              validationInterval="30000"

              removeAbandoned="true"
              removeAbandonedTimeout="300"
              logAbandoned="true"

              timeBetweenEvictionRunsMillis="30000"
              minEvictableIdleTimeMillis="60000"
              defaultAutoCommit="true" />
</Context>

The application looks the pool up as java:comp/env/jdbc/AppDS. Confirm the exact name with the development team.

URL format: @//host:port/SERVICE_NAME is the modern form and works with pluggable databases. The old @host:port:SID form cannot connect to a PDB.

Understanding the pool settings

These are sensible starting values for a medium application, not magic numbers. Each card explains what the setting controls.

SIZE
initialSize = 10
Connections opened when the pool starts.
SIZE
minIdle = 10
The pool keeps at least this many connections open, ready for traffic.
SIZE
maxIdle = 50
Idle connections above this number are closed.
SIZE
maxActive = 100
Hard ceiling on connections in use at the same time. Size the database for it.
SIZE
maxWait = 10000
How long (ms) a request waits for a free connection before it gets an error.
VALIDATE
validationQuery = SELECT 1 FROM DUAL
The cheapest possible query to prove a connection is alive.
VALIDATE
testOnBorrow = true
Check the connection before handing it to the application.
VALIDATE
validationInterval = 30000
Validate the same connection at most once every 30 s, so checks stay cheap.
LEAKS
removeAbandoned = true
Reclaim connections the application forgot to close.
LEAKS
removeAbandonedTimeout = 300
Seconds in use before a connection is treated as leaked. Must be longer than your longest query.
LEAKS
logAbandoned = true
Log the stack trace of the code that leaked the connection, so it can be fixed.
SWEEPER
timeBetweenEvictionRunsMillis = 30000
How often the background sweeper runs to test idle connections and look for leaks.
SWEEPER
minEvictableIdleTimeMillis = 60000
Idle time before a connection above minIdle may be closed.
Size the database, not just the pool. Every Tomcat instance can open up to maxActive sessions. Three instances at 100 each means 300 extra sessions on Oracle, so check PROCESSES and SESSIONS first:
SQL> SELECT resource_name, current_utilization, max_utilization, limit_value
     FROM   v$resource_limit
     WHERE  resource_name IN ('processes','sessions');
Why not SELECT * FROM some_table? A validation query runs thousands of times a day. Reading a real table adds needless load and fails if the table is ever renamed. SELECT 1 FROM DUAL touches nothing.

STEP 6
Start Tomcat and check the log

sudo -u tomcat $CATALINA_HOME/bin/startup.sh
tail -f $CATALINA_HOME/logs/catalina.out
Look for
INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [2,481] milliseconds

Then open http://<server-ip>:8080/. You should see the Tomcat welcome page. If not, check the firewall: sudo firewall-cmd --add-port=8080/tcp --permanent && sudo firewall-cmd --reload.

Different port? The HTTP port is set on the <Connector port="8080" protocol="HTTP/1.1"> line in conf/server.xml. Change it there if 8080 is taken.

STEP 7
Deploy the WAR

With the default autoDeploy="true" on the <Host>, copying the WAR into webapps is all it takes. Tomcat unpacks and starts it within seconds.

scp sample-app.war tomcat@appserver:/tmp/          # from your machine (or WinSCP)

sudo -u tomcat cp /tmp/sample-app.war $CATALINA_HOME/webapps/
ls -l $CATALINA_HOME/webapps/
grep -E "Deploying web application archive|has finished|SEVERE" $CATALINA_HOME/logs/catalina.out | tail

The WAR file name becomes the URL path: sample-app.war is served at http://<server-ip>:8080/sample-app/. Rename it to ROOT.war to serve it at the root path.

curl -I http://localhost:8080/sample-app/

STEP 8
Run Tomcat as a systemd service

So that Tomcat starts at boot and can be managed like any other service:

/etc/systemd/system/tomcat.service
[Unit]
Description=Apache Tomcat 9
After=network.target

[Service]
Type=forking
User=tomcat
Group=tomcat
Environment=CATALINA_HOME=/opt/tomcat/latest
Environment=CATALINA_BASE=/opt/tomcat/latest
ExecStart=/opt/tomcat/latest/bin/startup.sh
ExecStop=/opt/tomcat/latest/bin/shutdown.sh
PIDFile=/opt/tomcat/latest/temp/tomcat.pid
Restart=on-failure

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now tomcat
sudo systemctl status tomcat

STEP 9
Stop Tomcat

sudo systemctl stop tomcat
# or, without systemd
sudo -u tomcat $CATALINA_HOME/bin/shutdown.sh

Troubleshooting

Message in catalina.outCauseFix
ClassNotFoundException: oracle.jdbc.OracleDriverojdbc8.jar is not in $CATALINA_HOME/libCopy it there and restart Tomcat
Name [jdbc/AppDS] is not bound in this ContextJNDI name mismatch, or a global resource without a ResourceLinkMatch the name the app uses; add a ResourceLink if you used server.xml
ORA-12514 / IO Error: Network AdapterWrong service name, host or port, or a firewallTest with nc -zv dbhost 1521 and sqlplus app_user@//dbhost:1521/ORCLPDB1
Timeout: Pool empty. Unable to fetch a connection in 10 secondsAll maxActive connections are busy, or leakingLook for logAbandoned stack traces; check slow SQL before raising maxActive
ORA-00020 maximum number of processes exceededPools across all instances exceed the DB limitLower maxActive, or raise PROCESSES on the database
Address already in use on startupPort 8080 (or 8005 shutdown, 8009 AJP) is takenss -tlnp | grep 8080; change the Connector port

Conclusion

Tomcat itself installs in minutes. The part that decides whether the application runs smoothly is the datasource. Use a service-name URL, keep the driver in lib, validate with SELECT 1 FROM DUAL, turn on leak detection, and size maxActive together with the database's PROCESSES limit, not in isolation.

What pool settings do you use in production? Share them in the comments.

Tamilselvan Perumal · Lead Product Engineer & Senior DBA