Skip to content
DBDeependra Bhatta~/notes
CI/CD#java · #maven · #jenkins · #tomcat

Build and Deploy a Java WAR to Tomcat with Maven and Jenkins

Build a Java web app with Maven, deploy the WAR to Tomcat by hand, then automate the same build and deploy with two chained Jenkins freestyle jobs.

· updated · 9 min read
ON THIS PAGE

Automating a deployment is easier once the manual steps are clear: build the WAR file, copy it to Apache Tomcat, and reload the app. This guide packages a Java web app with Maven, deploys it to Tomcat by hand, and then automates the same flow with two chained Jenkins freestyle jobs, one that builds and archives the WAR and one that deploys it.

Prerequisites

  • An Ubuntu VM with sudo access. In this lab the same VM also runs Jenkins on port 8080.
  • Jenkins installed as described in Jenkins Basics.
  • A GitHub repository to hold the app code.

Maven in short

Apache Maven is a build tool for Java. You describe the project in a pom.xml file (POM means Project Object Model): its name, version, dependencies and plugins. Maven then downloads the dependencies, compiles, tests and packages the code.

Build lifecycle

Maven runs a fixed sequence of phases. Running one phase runs every phase before it, so mvn package also validates, compiles and tests.

PhaseWhat it does
validateChecks that the project is correct and all needed information is there.
compileCompiles the source code (.java files into .class bytecode).
testRuns unit tests on the compiled code. Nothing is packaged yet.
packageBundles the compiled code into a distributable file such as a JAR or WAR.
verifyRuns checks on integration test results to make sure quality criteria are met.
installCopies the package into the local repository (~/.m2) so other local projects can use it.
deployCopies the package to a remote repository to share with other developers.

clean is a separate lifecycle that deletes the target folder, which is why builds often run mvn clean package.

Install Java and Maven

Maven needs a full JDK, not only a JRE, to compile code:

terminal
$ sudo apt update
$ sudo apt install openjdk-21-jdk maven
$ mvn --version

Create and package the web app

Generate a web app skeleton from the Maven webapp archetype (a project template):

terminal
$ mvn archetype:generate -DarchetypeArtifactId=maven-archetype-webapp
Define value for property 'groupId': Dipen
Define value for property 'artifactId': dipendra
Define value for property 'version' 1.0-SNAPSHOT: v 2.0.0
Define value for property 'package' Dipen: war
  • groupId is the group or organisation (usually a reversed domain, like np.com.example).
  • artifactId is the project name and the folder that gets created.
  • package is the Java package name. This archetype creates only a JSP page and no Java classes, so the value has no effect here.

Confirm with y. Maven creates the pom.xml and a simple "Hello World" JSP app.

Add the WAR plugin and a fixed output file name inside <project> in pom.xml:

XMLpom.xml
<build>
  <finalName>dipendra_webapp</finalName>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-war-plugin</artifactId>
      <version>3.4.0</version>
    </plugin>
  </plugins>
</build>

Build the package:

terminal
$ mvn package
$ ls target/*.war
target/dipendra_webapp.war

Set up Tomcat

Apache Tomcat is a free, open source web server and servlet container for Java web apps. Tomcat 11 needs Java 17 or later.

Download the Core binary tar.gz from the Tomcat 11 download page. Use the binary distribution, not the -src source archive, which must be built before it can run. This guide uses 11.0.11, extracted to /opt/apache-tomcat1:

terminal
$ wget https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.11/bin/apache-tomcat-11.0.11.tar.gz
$ tar xzvf apache-tomcat-11.0.11.tar.gz
$ sudo mv apache-tomcat-11.0.11 /opt/apache-tomcat1
$ cd /opt/apache-tomcat1/bin
$ sudo ./startup.sh
$ sudo ./shutdown.sh

The .sh scripts are for Linux and the .bat scripts for Windows. Both start Catalina, the main Tomcat process. To run Tomcat as a proper service that starts on boot and restarts on failure, see Running Tomcat as a systemd Service.

The key directories:

FolderContents
binStart, stop and helper scripts.
confConfiguration: server.xml, tomcat-users.xml and others.
webappsDeployed apps. A WAR file copied here is deployed automatically.
logscatalina.out and access logs. Check here first when something fails.

Configure Tomcat before deploying

Change the port

Jenkins already listens on 8080 on this VM, so move the Tomcat HTTP connector to 8081:

±server.xml+1−1
-    <Connector port="8080" protocol="HTTP/1.1"
+    <Connector port="8081" protocol="HTTP/1.1"

If you run more than one Tomcat on the same machine, each instance also needs its own shutdown port (default 8005):

±server.xml+1−1
-<Server port="8005" shutdown="SHUTDOWN">
+<Server port="8010" shutdown="SHUTDOWN">

Add manager users

The Manager app (/manager/html) lists, deploys and removes apps. It requires a user with the correct roles. Create two users: one for browser access, and one that Jenkins uses to deploy through the Manager's text API.

XMLtomcat-users.xml
conftomcat-users.xml
<tomcat-users>
  <role rolename="manager-gui"/>
  <role rolename="admin-gui"/>
  <role rolename="manager-script"/>
  <user username="admin" password="CHANGE_ME" roles="manager-gui,admin-gui"/>
  <user username="tomcat" password="CHANGE_ME" roles="manager-script"/>
</tomcat-users>

Allow remote access to the Manager and Host Manager

By default the Manager and Host Manager apps accept requests only from localhost. A RemoteAddrValve in each app's context.xml enforces this:

  • /opt/apache-tomcat1/webapps/manager/META-INF/context.xml
  • /opt/apache-tomcat1/webapps/host-manager/META-INF/context.xml

In my lab I commented out the valve in both files, which allows access from any IP:

XMLcontext.xml
webappsmanagerMETA-INFcontext.xml
<Context antiResourceLocking="false" privileged="true">
  <!-- <Valve className="org.apache.catalina.valves.RemoteAddrValve"
         allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" /> -->
  ...
</Context>

A safer option is to keep the valve and add only your own network to allow, for example a VirtualBox host-only network:

±context.xml+1−1
-         allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />
+         allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1|192\.168\.56\.\d+" />

Restart Tomcat to apply the changes:

terminal
$ cd /opt/apache-tomcat1/bin
$ sudo ./shutdown.sh
$ sudo ./startup.sh

Deploy the WAR by hand

Copy the WAR into webapps. Tomcat extracts it into a folder with the same name, without .war:

terminal
$ sudo cp target/dipendra_webapp.war /opt/apache-tomcat1/webapps/
$ ls /opt/apache-tomcat1/webapps

The app now appears in the Manager app as /dipendra_webapp and opens at http://192.168.56.150:8081/dipendra_webapp/.

To serve it at the root URL (http://192.168.56.150:8081/) instead, replace the default ROOT app:

terminal
$ sudo rm -rf /opt/apache-tomcat1/webapps/ROOT
$ sudo cp target/dipendra_webapp.war /opt/apache-tomcat1/webapps/ROOT.war

Automate it with Jenkins freestyle jobs

Before creating the Jenkins jobs, confirm the following:

  • Maven and a JDK must be on the Jenkins machine (installed as above, or set up under Manage Jenkins > Tools).
  • Tomcat must be set up as above, with the manager-script user for Jenkins.
  • The app code must be in a GitHub repository.

Job 1: build and archive the WAR

Create a freestyle job named maven_deploy:

  1. Source Code Management: Git, the repository URL, credentials if the repo is private, and the branch.
  2. Build Steps: Invoke top-level Maven targets with goals clean package. If pom.xml is not at the repo root, set its path under Advanced > POM.
  3. Post-build Actions: Archive the artifacts with **/*.war, which matches the WAR anywhere in the workspace and saves it with the build.

Freestyle job with Maven goals clean package and an Archive the artifacts step for **/*.war

After a successful build, the WAR appears as an artifact on the build page.

Job 2: deploy the WAR to Tomcat

Install two plugins from Manage Jenkins > Plugins, then restart Jenkins:

  • Copy Artifact: copies artifacts from another job's build.
  • Deploy to container: deploys a WAR or EAR to Tomcat and other servers through their manager API.
terminal
$ sudo systemctl restart jenkins

Then create a second freestyle job, deploy_tomcat_using_maven.

Build step: Copy artifacts from another project, taking **/*.war from the latest successful (stable) build of maven_deploy:

Copy artifacts build step taking **/*.war from the latest stable build of the maven_deploy job

Post-build action: Deploy war/ear to a container:

Deploy war/ear to a container step with context path /, Tomcat 9.x Remote, saved credentials and Tomcat URL on port 8081

  • WAR/EAR files: **/*.war.
  • Context path: the URL path the app is deployed under. / replaces the ROOT app, so the site opens at the Tomcat root. A name like myapp deploys it as myapp.war next to ROOT, and it opens at http://192.168.56.150:8081/myapp/. If left blank, the WAR file name is used.
  • Containers: Tomcat 9.x Remote worked for my Tomcat 11, with the Tomcat URL http://192.168.56.150:8081.
  • Credentials: click Add and create a Username with password credential with the manager-script user, an ID and a description, then select it.

Save and build. The app is now live at the Tomcat URL.

Chain the two jobs

Rather than start the deploy job by hand, have the build job trigger it. In maven_deploy, add the post-build action Build other projects and pick Trigger only if build is stable:

Post-build actions archiving **/*.war and building deploy_tomcat_using_maven only if the build is stable

Now one build of maven_deploy builds the WAR and then deploys it. maven_deploy is the upstream job and deploy_tomcat_using_maven is the downstream job.

Manual approval before deploying

To keep the deploy as a manual step, use the Build Pipeline plugin. Its post-build action Build other projects (manual step) adds the downstream job, which runs only when someone starts it. The Build Pipeline plugin is no longer actively maintained (it is up for adoption), so for new work a pipeline input step is the better choice. The next part uses one.

Troubleshooting

  • Tomcat does not start or the port is taken: another service, here Jenkins, is on 8080. Change the connector port in server.xml.
  • 403 Access Denied on /manager/html from another machine: the RemoteAddrValve in the Manager's context.xml still allows only localhost.

Key takeaways

  • mvn clean package runs every lifecycle phase up to package and leaves the WAR in target/.
  • Tomcat deploys any WAR copied into webapps. Name it ROOT.war to serve it at /.
  • Give Jenkins its own Tomcat user with only manager-script, and keep the Manager restricted by IP.
  • Splitting build and deploy into upstream and downstream jobs lets you rerun each step on its own.
  • Freestyle chains work, but a pipeline defined in code is easier to review and extend.

Next in this series: Jenkins Pipeline for Building and Deploying Docker Images.