Deploying Apache Tomcat and Running a WAR File on AWS ECS
Author: Harvinder Singh Saluja
Tags: #AWS #ECS #Tomcat #DevOps #Java #WARDeployment
Modern applications demand scalable and resilient infrastructure. Apache Tomcat, a popular Java servlet container, can be containerized and deployed on AWS ECS (Elastic Container Service) for high availability and manageability. In this blog, we walk through the end-to-end process of containerizing Tomcat with a custom WAR file and deploying it on AWS ECS using Fargate.
Objective
To deploy a Java .war
file under Tomcat on AWS ECS Fargate and access the web application through an Application Load Balancer (ALB).
Prerequisites
-
AWS Account
-
Docker installed locally
-
AWS CLI configured
-
An existing
.war
file (e.g.,myapp.war
) -
Basic understanding of ECS, Docker, and networking on AWS
Step 1: Create Dockerfile for Tomcat + WAR
Create a Dockerfile
to extend the official Tomcat image and copy the WAR file into the webapps directory.
# Use official Tomcat base image
FROM tomcat:9.0
# Remove default ROOT webapp
RUN rm -rf /usr/local/tomcat/webapps/ROOT
# Copy custom WAR file
COPY myapp.war /usr/local/tomcat/webapps/ROOT.war
# Expose port
EXPOSE 8080
# Start Tomcat
CMD ["catalina.sh", "run"]
Place this Dockerfile alongside your myapp.war
.
Step 2: Build and Push Docker Image to Amazon ECR
-
Create ECR Repository
aws ecr create-repository --repository-name tomcat-myapp
-
Authenticate Docker with ECR
aws ecr get-login-password | docker login --username AWS --password-stdin <aws_account_id>.dkr.ecr.<region>.amazonaws.com
-
Build and Push Docker Image
docker build -t tomcat-myapp .
docker tag tomcat-myapp:latest <aws_account_id>.dkr.ecr.<region>.amazonaws.com/tomcat-myapp:latest
docker push <aws_account_id>.dkr.ecr.<region>.amazonaws.com/tomcat-myapp:latest
Step 3: Setup ECS Cluster and Fargate Service
-
Create ECS Cluster
aws ecs create-cluster --cluster-name tomcat-cluster
-
Create Task Definition JSON
Example: task-def.json
{
"family": "tomcat-task",
"networkMode": "awsvpc",
"containerDefinitions": [
{
"name": "tomcat-container",
"image": "<aws_account_id>.dkr.ecr.<region>.amazonaws.com/tomcat-myapp:latest",
"portMappings": [
{
"containerPort": 8080,
"hostPort": 8080,
"protocol": "tcp"
}
],
"essential": true
}
],
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::<account_id>:role/ecsTaskExecutionRole"
}
-
Register Task Definition
aws ecs register-task-definition --cli-input-json file://task-def.json
-
Create Security Group & ALB
-
Create a security group allowing HTTP (port 80) and custom port 8080.
-
Create an Application Load Balancer with a target group pointing to port 8080.
-
-
Run ECS Fargate Service
aws ecs create-service \
--cluster tomcat-cluster \
--service-name tomcat-service \
--task-definition tomcat-task \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration '{
"awsvpcConfiguration": {
"subnets": ["subnet-xxxxxxx"],
"securityGroups": ["sg-xxxxxxx"],
"assignPublicIp": "ENABLED"
}
}' \
--load-balancers '[
{
"targetGroupArn": "arn:aws:elasticloadbalancing:<region>:<account_id>:targetgroup/<target-group-name>",
"containerName": "tomcat-container",
"containerPort": 8080
}
]'
Step 4: Access the Deployed App
Once the ECS service stabilizes, navigate to the DNS name of the ALB (e.g., http://<alb-dns-name>
) to access your Java application running on Tomcat.
Troubleshooting Tips
-
WAR not deploying? Make sure it's named
ROOT.war
if you want it accessible directly at/
. -
Service unhealthy? Confirm security group rules allow traffic on port 8080.
-
Task failing? Check ECS task logs in CloudWatch.
A CloudFormation Template (CFT) was revised to deploy Apache Tomcat on ECS Fargate using private subnets. In this version:
-
Tomcat runs in private subnets
-
Application Load Balancer (ALB) resides in public subnets
-
NAT Gateway is used to allow ECS tasks to access the internet (e.g., for downloading updates)
-
WAR file is pre-packaged into the Docker image
-
Load Balancer forwards traffic to ECS service running in private subnets
tomcat-on-ecs-private.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy Tomcat in ECS Fargate with WAR file under private subnets and public-facing ALB
Parameters:
VpcCidr:
Type: String
Default: 10.0.0.0/16
PublicSubnet1Cidr:
Type: String
Default: 10.0.1.0/24
PublicSubnet2Cidr:
Type: String
Default: 10.0.2.0/24
PrivateSubnet1Cidr:
Type: String
Default: 10.0.3.0/24
PrivateSubnet2Cidr:
Type: String
Default: 10.0.4.0/24
ImageUrl:
Type: String
Description: ECR image URL for the Tomcat + WAR image
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
# Subnets
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PublicSubnet1Cidr
AvailabilityZone: !Select [ 0, !GetAZs '' ]
MapPublicIpOnLaunch: true
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PublicSubnet2Cidr
AvailabilityZone: !Select [ 1, !GetAZs '' ]
MapPublicIpOnLaunch: true
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PrivateSubnet1Cidr
AvailabilityZone: !Select [ 0, !GetAZs '' ]
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PrivateSubnet2Cidr
AvailabilityZone: !Select [ 1, !GetAZs '' ]
InternetGateway:
Type: AWS::EC2::InternetGateway
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicRouteAssoc1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicRouteAssoc2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
# NAT Gateway setup for private subnets
EIP:
Type: AWS::EC2::EIP
NATGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt EIP.AllocationId
SubnetId: !Ref PublicSubnet1
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGateway
PrivateRouteAssoc1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateRouteAssoc2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
# Security Group
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow inbound traffic from ALB
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
SourceSecurityGroupId: !Ref ALBSecurityGroup
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP from internet
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
# ALB
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Subnets: [!Ref PublicSubnet1, !Ref PublicSubnet2]
SecurityGroups: [!Ref ALBSecurityGroup]
Scheme: internet-facing
Type: application
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Port: 8080
Protocol: HTTP
VpcId: !Ref VPC
TargetType: ip
HealthCheckPath: /
HealthCheckPort: 8080
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
ECSCluster:
Type: AWS::ECS::Cluster
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: tomcat-task
Cpu: 512
Memory: 1024
NetworkMode: awsvpc
RequiresCompatibilities: [FARGATE]
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
- Name: tomcat-container
Image: !Ref ImageUrl
PortMappings:
- ContainerPort: 8080
Essential: true
ECSService:
Type: AWS::ECS::Service
DependsOn: Listener
Properties:
Cluster: !Ref ECSCluster
DesiredCount: 1
LaunchType: FARGATE
TaskDefinition: !Ref TaskDefinition
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
Subnets: [!Ref PrivateSubnet1, !Ref PrivateSubnet2]
SecurityGroups: [!Ref ECSSecurityGroup]
LoadBalancers:
- TargetGroupArn: !Ref TargetGroup
ContainerName: tomcat-container
ContainerPort: 8080
Outputs:
ALBDNS:
Description: DNS of the Application Load Balancer
Value: !GetAtt LoadBalancer.DNSName
Deploy Instructions
-
Save this file as
tomcat-on-ecs-private.yaml
-
Deploy using AWS CLI:
aws cloudformation deploy \
--template-file tomcat-on-ecs-private.yaml \
--stack-name tomcat-private-ecs \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides ImageUrl=<your-ecr-image-url>
-
Once stack creation is complete, access the application via the ALB DNS output.
Would you like this exported to PDF, or want a GitHub Actions pipeline to automate container builds and deployments?
Conclusion
You’ve now deployed a containerized Tomcat server running a WAR application to AWS ECS using Fargate. This setup abstracts away server management, allowing you to focus on your application logic while AWS handles the infrastructure.