Skip to main content

Command Palette

Search for a command to run...

Deploying Java Applications to Tomcat via Jenkins

Building a WAR file is only half the job. The other half is getting that WAR file onto a server where it can actually run and serve users. That server, for Java web applications, is Apache Tomcat. In this post, we will set up Tomcat on a dedicated EC2 instance, configure it for Jenkins access, install the deployment plugin, and write the pipeline code to automate the entire deployment process.

Updated
7 min read
Deploying Java Applications to Tomcat via Jenkins
S
Passionate about Cloud and DevOps engineering. I write structured technical notes and beginner-friendly articles on AWS, Linux, CI/CD, networking, system architecture and modern software delivery workflows.

The Deployment Architecture

At this point in our CI/CD pipeline, here is what we have:

  1. Developer pushes code to GitHub.

  2. Jenkins pulls the code, Maven builds it, and a WAR file is generated in the workspace.

What we need to add:

  1. Jenkins takes the WAR file and deploys it to a Tomcat server running on a separate EC2 instance.

  2. Users access the application through Tomcat.

The Tomcat server is a separate machine from Jenkins. This is the correct way to set up a deployment environment -- you do not run your application on the same server as your CI tool.


Setting Up Tomcat Automatically with a Script

Previously, the Tomcat setup was done manually step by step. For efficiency, the entire setup is captured in a shell script (tomcat.sh). Let us walk through what it does so you understand each step, even when running it automatically.

#!/bin/bash

# Install Java 21 (required by Tomcat)
dnf install java-21-amazon-corretto -y

# Download Tomcat 11
wget https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.14/bin/apache-tomcat-11.0.14.tar.gz

# Extract it
tar -zxvf apache-tomcat-11.0.14.tar.gz

# Add the manager-gui role
sed -i '56 a\<role rolename="manager-gui"/>' apache-tomcat-11.0.14/conf/tomcat-users.xml

# Add the manager-script role
sed -i '57 a\<role rolename="manager-script"/>' apache-tomcat-11.0.14/conf/tomcat-users.xml

# Add a user with both roles
sed -i '58 a\<user username="BHATTU" password="BHATTU" roles="manager-gui, manager-script"/>' apache-tomcat-11.0.14/conf/tomcat-users.xml

# Close the tomcat-users block properly
sed -i '59 a\</tomcat-users>' apache-tomcat-11.0.14/conf/tomcat-users.xml

# Remove the closing tag that was already there to avoid duplication
sed -i '56d' apache-tomcat-11.0.14/conf/tomcat-users.xml

# Remove the IP restriction lines from the Manager app context
sed -i '21d' apache-tomcat-11.0.14/webapps/manager/META-INF/context.xml
sed -i '22d' apache-tomcat-11.0.14/webapps/manager/META-INF/context.xml

# Start Tomcat
sh apache-tomcat-11.0.14/bin/startup.sh

Let us unpack the important parts.


Why We Edit tomcat-users.xml

By default, Tomcat has no users configured. The Manager application (which lets you deploy, undeploy, and manage web applications) is locked down. Jenkins needs a user account with the right roles to deploy WAR files through the Manager API.

The two roles we add are:

  • manager-gui -- allows access to the Tomcat Manager web interface in the browser.

  • manager-script -- allows programmatic access (via HTTP API), which is how Jenkins deploys WAR files.

The user we create (BHATTU with password BHATTU in this example) gets both roles. You should use a stronger username and password in any real environment.


Why We Edit context.xml

Tomcat's Manager application has a built-in restriction that only allows connections from localhost (IP address 127.0.0.1). This is a security default.

The lines we delete from webapps/manager/META-INF/context.xml are:

<Valve className="org.apache.catalina.valves.RemoteAddrValve"
       allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />

Removing these lines allows connections from any IP address, including the Jenkins server. Without this change, Jenkins would get a 403 Forbidden error every time it tried to deploy.


Running the Setup Script

On your Tomcat EC2 instance (a fresh Amazon Linux 2 machine):

chmod +x tomcat.sh
sudo bash tomcat.sh

Important: Check the current Tomcat version on the Apache Tomcat download page. The script uses 11.0.14 -- if a newer patch version is available, update the version number in the script accordingly before running it.

After the script finishes, you will see:

Tomcat started.

Verifying Tomcat Is Running

Take the public IP of your Tomcat EC2 instance and open it in a browser:

http://<tomcat-ec2-public-ip>:8080

You should see the Tomcat welcome page. Click Manager App and enter the username (BHATTU) and password (BHATTU). You will see the Tomcat Web Application Manager, which lists all deployed applications.

This is where our WAR file will appear after Jenkins deploys it.


Installing the Deploy to Container Plugin in Jenkins

Jenkins does not know how to talk to Tomcat's deployment API natively. We need a plugin.

  1. Go to Manage Jenkins.

  2. Click Plugins.

  3. Click Available plugins.

  4. Search for Deploy to container.

  5. Select Deploy to container Plugin.

  6. Click Install.

  7. Check Restart Jenkins when installation is complete.

This plugin adds a new post-build action (and a new pipeline step) that Jenkins can use to deploy WAR files to Tomcat, JBoss, and other Java application servers.


Adding Tomcat Credentials to Jenkins

Jenkins needs to authenticate with Tomcat's Manager API to deploy. Store the Tomcat credentials securely in Jenkins:

  1. Go to Manage Jenkins.

  2. Click Credentials.

  3. Click System, then Global credentials (unrestricted).

  4. Click Add Credentials.

  5. Fill in:

    • Kind: Username with password

    • Username: BHATTU

    • Password: BHATTU

    • ID: tomcat-creds (or any descriptive name)

    • Description: Tomcat Manager credentials

  6. Click Create.


Option A: Freestyle Project with Post-Build Deployment

If you are using a Freestyle project, deployment is configured as a post-build action.

In the job configuration:

  1. Scroll to Post-build Actions.

  2. Click Add post-build action.

  3. Select Deploy war/ear to a container.

  4. Fill in:

    • WAR/EAR files: **/*.war (finds any WAR file in the workspace)

    • Context path: my-web-app (the URL path users will use, e.g., http://ip:8080/my-web-app)

    • Containers: Click Add Container, select Tomcat 9.x Remote (the closest available option)

    • Credentials: Select tomcat-creds

    • Tomcat URL: http://<tomcat-ec2-public-ip>:8080

Click Save, then Build Now.

Jenkins will:

  1. Pull the code from GitHub.

  2. Build the WAR with Maven.

  3. Send the WAR to Tomcat's deployment API.

  4. Tomcat installs and starts the application.

After the build succeeds, go back to the Tomcat Manager page and refresh. You will see your application listed.


Option B: Pipeline-Based Deployment

For declarative pipelines, use the Pipeline Syntax generator to create the deployment step:

  1. Inside your pipeline job, click Pipeline Syntax in the sidebar.

  2. From the Sample Step dropdown, select deploy: Deploy war/ear to a container.

  3. Fill in the same fields as above (WAR path, context path, container type, credentials, Tomcat URL).

  4. Click Generate Pipeline Script.

Jenkins generates something like:

deploy adapters: [tomcat9(credentialsId: 'tomcat-creds',
                           path: '',
                           url: 'http://<tomcat-ip>:8080')],
       contextPath: 'my-web-app',
       war: '**/*.war'

Copy this and add it as a step inside a Deploy stage in your pipeline:

pipeline {
    agent any
    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/your-username/java-app.git'
            }
        }
        stage('Compile') {
            steps {
                sh 'mvn compile'
            }
        }
        stage('Test') {
            steps {
                sh 'mvn test'
            }
        }
        stage('Package') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('Deploy to Tomcat') {
            steps {
                deploy adapters: [tomcat9(credentialsId: 'tomcat-creds',
                                           path: '',
                                           url: 'http://<tomcat-ip>:8080')],
                       contextPath: 'my-web-app',
                       war: '**/*.war'
            }
        }
    }
}

Click Save, then Build Now.


Seeing the Deployed Application

After a successful build and deployment:

  1. Open the Tomcat Manager at http://<tomcat-ip>:8080/manager/html.

  2. You will see my-web-app listed under Applications.

  3. Click the link or navigate directly to http://<tomcat-ip>:8080/my-web-app.

Your Java web application is now live and served by Tomcat.


What the WAR File Path **/*.war Means

The path **/*.war uses Ant-style globbing:

  • ** matches any directory depth (any subdirectory, including nested ones)

  • *.war matches any file ending in .war

Combined, **/*.war finds the WAR file no matter which subdirectory it is in. Since Maven always puts the WAR in target/, the actual WAR file being picked up is target/my-app.war (or whatever your project's artifact ID is).


Summary

The complete deployment flow now looks like this:

  1. Developer commits code to GitHub.

  2. Jenkins detects the commit (via webhook or Poll SCM).

  3. Jenkins clones the repository.

  4. Maven compiles, tests, and packages the code into a WAR file.

  5. Jenkins uses the Deploy to Container plugin to send the WAR to Tomcat.

  6. Tomcat deploys and starts the application.

  7. Users access the application through the Tomcat URL.

Key points to remember:

  • Tomcat requires Java and a configured user in tomcat-users.xml with manager-gui and manager-script roles.

  • The IP restriction in context.xml must be removed to allow Jenkins to connect remotely.

  • The Deploy to container Plugin enables Jenkins to talk to Tomcat's deployment API.

  • Credentials are stored securely in Jenkins and referenced by ID in job or pipeline configuration.

  • The Pipeline Syntax generator creates the correct Groovy deployment code for you.

In the next posts, we will cover storing your build artifacts in AWS S3 and Nexus, and then running code quality analysis with SonarQube.

More from this blog

Ansible Playbooks Explained: From First YAML File to Managing Real Servers

If you have already run a few Ansible ad hoc commands and seen how they work, you already understand the core idea: one command, many servers. But ad hoc commands only take you so far. When you need to install software, start services, create users, copy files, and print confirmation messages, all in one automated run across multiple servers, that is when you move to playbooks. Playbooks are where Ansible truly earns its place in a DevOps workflow. Everything you do in Ansible at scale, you do through playbooks.

Jun 19, 202613 min read
Ansible Playbooks Explained: From First YAML File to Managing Real Servers

You Have 400 Servers to Configure. Now What? Let Ansible Do the Work.

Picture this. You have four EC2 instances running in your AWS account, and someone asks you to install Apache on all four of them. What do you do? The obvious answer most people go with is SSH into each machine, run the install command, repeat. Simple enough when it is four servers. But what happens when it is forty? Or four hundred? In a real enterprise environment, that number is not exaggerated at all. That is exactly the problem that Ansible was built to solve. And once you understand what it does and how it thinks, you will wonder how anyone managed large infrastructure without it.

Jun 18, 202612 min read
You Have 400 Servers to Configure. Now What? Let Ansible Do the Work.

Building Production-Ready Docker Deployments with Secrets, Stacks, and Distroless Images

This post wraps up the core Docker Swarm curriculum by covering four important topics that complete the picture of production-ready containerized deployments. We start with Docker Secrets, which solves the real-world problem of passing sensitive credentials into containers without hardcoding them. We then look at Docker Stack, which is how you run multi-service Docker Compose files across a Swarm cluster instead of a single host. After that we cover the distinction between replicated and global services, which is a concept that appears in Kubernetes as well. We close with a look at Portainer for those who prefer a visual interface, and a brief introduction to distroless images. Each of these topics builds on everything covered so far. If you have your Swarm cluster running, you can follow along with every command shown here.

Jun 17, 202614 min read
Building Production-Ready Docker Deployments with Secrets, Stacks, and Distroless Images

Beyond One Server: Solving Docker Scaling with Swarm and Container Networks

This post covers three separate but deeply connected topics. We start by finishing what was started with Docker Hub, pushing all four bank service images to a remote registry so they survive beyond any single machine. From there, we identify a real architectural problem with single-host Docker deployments and introduce Docker Swarm as the solution. Finally, we close with Docker networking, explaining how containers communicate with each other both on the same host and across different hosts. By the end of this article, you will understand how to push and pull images from Docker Hub, how to set up a multi-node Docker Swarm cluster, how to create and scale services across that cluster, what self-healing means in practice, and how Docker networking works under the hood.

Jun 17, 202618 min read
Beyond One Server: Solving Docker Scaling with Swarm and Container Networks

Docker Compose in Action: Multi-Container Apps, Nginx Load Balancing & Docker Hub

We built four containerized microservices for an bank application in the last post. Internet banking, mobile banking, insurance, and loans, each running in its own container, each exposed on a separate port. The setup worked. But the process of building and running each container individually by hand was repetitive, error-prone, and simply not practical at scale. This post introduces Docker Compose, and by the end, you will understand not just how to use it, but why it exists, what its real limitations are, how to combine it with Nginx to build a working high availability architecture, and how to push your images to Docker Hub so they are available beyond your local machine. There is also a hands-on project included here that builds a Flask-based Python application behind an Nginx load balancer, which you are expected to complete as a practical exercise.

Jun 16, 202617 min read
Docker Compose in Action: Multi-Container Apps, Nginx Load Balancing & Docker Hub
S

Sai Praneeth's Blogs

37 posts

From SDLC and Agile to DevOps and CI/CD, this blog is where I share structured technical notes, concepts and practical insights in Cloud and DevOps engineering.