Apache TomEE 9.1.3 on AWS User Guide
Overview
This guide covers launching and using the Apache TomEE 9.1.3 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Java application servers listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Apache TomEE is the Jakarta EE application server from the Apache Software Foundation, built on Apache Tomcat. This image ships the TomEE 9.1.3 "plus" distribution, the Jakarta EE 9.1 stack (Servlet, JSP, JSF, CDI, EJB, JPA, JAX-RS, JAX-WS, JMS, JTA, Bean Validation and more) on Apache Tomcat 10.0.27 and OpenJDK 17. TomEE 9.1.3 is the final release of the 9.x line; Apache now develops TomEE 10 and 11.
What the cloudimg image adds to the upstream download:
- HTTPS from the first boot. nginx serves TomEE on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS, and requests for any host name the instance does not serve are dropped.
- No default login. The image ships with no Manager user at all. First boot creates two users with passwords unique to your instance:
adminfor the Manager web interface anddeployerfor scripted deployments. TomEE stores only PBKDF2 hashes of them, and locks out a user after repeated wrong passwords. The plain passwords are in/root/tomee-credentials.txt, readable by root only. - Fail closed. TomEE refuses to start unless every Manager user has a hashed password, and nginx refuses to start until first boot has finished.
- TomEE on loopback. TomEE's only connector listens on
127.0.0.1:8080behind nginx. The shutdown port is disabled, there is no AJP connector, and the documentation, examples and host manager applications are not deployed. Error pages do not show the server version. - The host name comes from configuration. TomEE and nginx are pinned to the instance's configured name, so applications build their URLs from it whatever headers a client sends. The client address TomEE logs and hands to your applications is the real address of the connection.
- Hardened defaults. TomEE runs as its own unprivileged user in a systemd sandbox, TomEE and nginx are blocked from the instance metadata service, and the AMI requires IMDSv2.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Apache TomEE 9.1.3 plus (/opt/tomee, the official Apache release, checksum verified), OpenJDK 17 (runtime only), nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. Your applications, configuration and logs live in /var/lib/tomee on a separate 30 GiB EBS data volume.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
- A security group that allows TCP 22 (SSH), TCP 443 (TomEE over HTTPS) and TCP 80 (redirects to HTTPS) from your own networks only.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Apache TomEE 9.1.3 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (or larger for bigger applications; TomEE sizes its Java heap to half of the instance's memory), your key pair, and a security group as above. The AMI brings two volumes: the root volume and the 30 GiB /var/lib/tomee data volume; enlarge the data volume at launch if your applications need more space.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=tomee}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Apache TomEE 9.1.3 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot generates the certificate and the passwords, then TomEE and nginx start:
systemctl is-active tomee nginx apache-tomee-firstboot
sudo test -f /var/lib/cloudimg/apache-tomee-firstboot.done && echo FIRSTBOOT_DONE
All three units are active and the first boot marker is present:
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on public addresses; TomEE listens on loopback only:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:8080 users:(("java"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
Step 4: Get your URL and passwords
sudo cat /root/tomee-credentials.txt
The file is readable by root only and holds the URLs, the two user names and this instance's passwords:
TOMEE_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
TOMEE_MANAGER_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/manager/html
TOMEE_ADMIN_USERNAME=admin
TOMEE_ADMIN_PASSWORD=<TOMEE_ADMIN_PASSWORD>
TOMEE_DEPLOYER_USERNAME=deployer
TOMEE_DEPLOYER_PASSWORD=<TOMEE_DEPLOYER_PASSWORD>
The URL uses the instance's public DNS name from EC2 instance metadata. The certificate also names the public and private IP addresses, so https://<public-ip>/ works too. To serve TomEE under your own DNS name, see Step 9.
Step 5: Open TomEE in your browser
Open the TOMEE_URL in your browser. The certificate is self signed, so your browser asks you to confirm it the first time (or install your own certificate, Step 9). The cloudimg welcome page links to the sample application and the Manager:

The sample application is a small JSP page compiled and served by your TomEE. It shows the server version, the address TomEE is served as (always your configured name) and your own address:

From the command line on the instance, the same checks:
H=$(sudo grep '^TOMEE_URL=' /root/tomee-credentials.txt | sed -E 's#^TOMEE_URL=https://([^/]+)/$#\1#')
R="--resolve $H:443:127.0.0.1 --resolve $H:80:127.0.0.1"
L=$(curl -s $R -o /dev/null -w '%{http_code} %{redirect_url}' "http://$H/"); echo "http:// -> $L"
[ "$L" = "301 https://$H/" ] || exit 1
P=$(curl -sk $R "https://$H/sample/"); echo "$P" | grep -E '<h1>|Server:|Served as:'
case "$P" in *"Served as: https://$H:443/sample/"*) ;; *) exit 1 ;; esac
X=$(curl -sk -o /dev/null -w '%{http_code}' -H 'Host: evil.example' https://127.0.0.1/ || true); echo "unknown Host -> $X (connection closed)"
[ "$X" = 000 ] || exit 1
http:// -> 301 https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
<body><h1>Sample application on Apache TomEE</h1>
<li>Server: Apache Tomcat (TomEE)/10.0.27 (9.1.3)</li>
<li>Served as: https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com:443/sample/</li>
unknown Host -> 000 (connection closed)
Step 6: Sign in to the Manager
Open the TOMEE_MANAGER_URL and sign in as admin with TOMEE_ADMIN_PASSWORD. The Manager lists the deployed applications (/, /manager and /sample) with buttons to start, stop, reload and undeploy each one, and a form to upload and deploy a WAR file:

Further down are the WAR upload form, diagnostics and the server information (TomEE 9.1.3 on Tomcat 10.0.27, OpenJDK 17):

Server Status shows the JVM memory pools and the loopback connector http-nio-127.0.0.1-8080, including the real client address of each request:

The Manager refuses a missing or wrong login, and the default tomcat/tomcat login does not exist. The admin user can use the web interface but not the text API, which belongs to deployer. The passwords are passed to curl on standard input, so they never appear in the process list or your shell history:
sudo bash -c '
F=/root/tomee-credentials.txt
H=$(grep "^TOMEE_URL=" $F | sed -E "s#^TOMEE_URL=https://([^/]+)/\$#\1#")
A=$(grep "^TOMEE_ADMIN_PASSWORD=" $F | cut -d= -f2-)
R="--resolve $H:443:127.0.0.1"
as() { printf "user = \"%s\"\n" "$1"; }
check() { echo "$1 -> HTTP $2"; [ "$2" = "$3" ] || exit 1; }
check "no login " "$(curl -sk $R -o /dev/null -w "%{http_code}" "https://$H/manager/html")" 401
check "wrong password " "$(as "admin:not-the-password" | curl -K - -sk $R -o /dev/null -w "%{http_code}" "https://$H/manager/html")" 401
check "tomcat/tomcat " "$(as "tomcat:tomcat" | curl -K - -sk $R -o /dev/null -w "%{http_code}" "https://$H/manager/html")" 401
check "admin, web UI " "$(as "admin:$A" | curl -K - -sk $R -o /dev/null -w "%{http_code}" "https://$H/manager/html")" 200
check "admin, text API" "$(as "admin:$A" | curl -K - -sk $R -o /dev/null -w "%{http_code}" "https://$H/manager/text/list")" 403
'
no login -> HTTP 401
wrong password -> HTTP 401
tomcat/tomcat -> HTTP 401
admin, web UI -> HTTP 200
admin, text API -> HTTP 403
After five wrong passwords in a row a user is locked out for five minutes.
Step 7: Deploy an application
Upload a WAR file in the Manager's WAR file to deploy form, or deploy from a script or CI pipeline with the deployer user and the Manager text API. This example builds a tiny WAR, deploys it at /hello, fetches it, lists the applications and undeploys it again:
sudo bash -c '
F=/root/tomee-credentials.txt
H=$(grep "^TOMEE_URL=" $F | sed -E "s#^TOMEE_URL=https://([^/]+)/\$#\1#")
D=$(grep "^TOMEE_DEPLOYER_PASSWORD=" $F | cut -d= -f2-)
R="--resolve $H:443:127.0.0.1"
dep() { printf "user = \"deployer:%s\"\n" "$D" | curl -K - -sk $R "$@"; }
W=$(mktemp -d); mkdir -p $W/hello
echo "<h1>Hello from my first TomEE deployment</h1>" > $W/hello/index.html
(cd $W/hello && python3 -c "import zipfile; z=zipfile.ZipFile(\"../hello.war\",\"w\"); z.write(\"index.html\"); z.close()")
OUT=$(dep -T $W/hello.war "https://$H/manager/text/deploy?path=/hello"); echo "$OUT"
case "$OUT" in "OK - Deployed"*) ;; *) exit 1 ;; esac
sleep 2
PAGE=$(curl -sk $R "https://$H/hello/"); echo "$PAGE"
case "$PAGE" in *"Hello from my first TomEE deployment"*) ;; *) exit 1 ;; esac
dep "https://$H/manager/text/list" | sort
OUT=$(dep "https://$H/manager/text/undeploy?path=/hello"); echo "$OUT"
case "$OUT" in "OK - Undeployed"*) ;; *) exit 1 ;; esac
rm -rf $W
'
OK - Deployed application at context path [/hello]
<h1>Hello from my first TomEE deployment</h1>
/:running:0:ROOT
/hello:running:0:hello
/manager:running:5:manager
/sample:running:8:sample
OK - Listed applications for virtual host [localhost]
OK - Undeployed application at context path [/hello]
From your own machine the same API works over HTTPS (-k accepts the self signed certificate):
curl -k -u deployer:<TOMEE_DEPLOYER_PASSWORD> -T ./myapp.war 'https://<public-ip>/manager/text/deploy?path=/myapp&update=true'
You can also copy a WAR into /var/lib/tomee/webapps over SSH (sudo install -o tomee -g tomee -m 0640 myapp.war /var/lib/tomee/webapps/); TomEE deploys it automatically. Deploy your application as ROOT.war to replace the welcome page at /.
Step 8: Security checks
The Manager users are stored as PBKDF2 hashes, never as plain passwords:
sudo grep '<user ' /var/lib/tomee/conf/tomcat-users.xml | sed -E 's/password="[^"]*"/password="<pbkdf2-hash>"/'
<user username="admin" password="<pbkdf2-hash>" roles="manager-gui,manager-status"/>
<user username="deployer" password="<pbkdf2-hash>" roles="manager-script"/>
TomEE's server configuration disables the shutdown port, binds the only connector to loopback, takes the host name from configuration and trusts forwarded client addresses from the local nginx only. The Manager answers only connections from that nginx:
sudo grep -E '<Server |<Connector |proxyName=|internalProxies=' /var/lib/tomee/conf/server.xml | sed 's/^ *//'
sudo grep -o 'usePeerAddress="true" allow="[^"]*"' /var/lib/tomee/webapps/manager/META-INF/context.xml
<Server port="-1" shutdown="SHUTDOWN">
<Connector port="8080" address="127.0.0.1" protocol="HTTP/1.1"
proxyName="${cloudimg.tomee.host}" proxyPort="443" scheme="https" secure="true" />
internalProxies="127\.0\.0\.1" trustedProxies=""
usePeerAddress="true" allow="127\.0\.0\.1"
nginx hands TomEE the configured host name and the real client address from the connection, never values a client sends:
grep -E 'proxy_set_header (Host|X-Forwarded-For|X-Real-IP)' /etc/nginx/tomee-proxy.conf
proxy_set_header Host $tomee_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
TomEE runs unprivileged in a sandbox, and neither TomEE nor nginx can reach the instance metadata service:
systemctl show tomee -p User -p ProtectSystem -p NoNewPrivileges -p IPAddressDeny
systemctl show nginx -p IPAddressDeny
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
User=tomee
ProtectSystem=strict
NoNewPrivileges=yes
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
The certificate was generated on this instance and names its own addresses:
H=$(sudo grep '^TOMEE_URL=' /root/tomee-credentials.txt | sed -E 's#^TOMEE_URL=https://([^/]+)/$#\1#')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost
nginx also refuses, with HTTP 403, a state changing request to the Manager whose Origin is another web site (browsers resend Basic Auth credentials on cross site requests), and a WebSocket upgrade from another web site to any application.
Step 9: Your own DNS name and certificate
Point a DNS record at the instance, then set the name TomEE is served as. This mints a new self signed certificate for the name, pins nginx and TomEE to it, updates the URLs in /root/tomee-credentials.txt, restarts TomEE and keeps the name across reboots:
sudo tomee-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running tomee-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/ssl/tomee/tomee.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/tomee/tomee.key
sudo nginx -t && sudo systemctl reload nginx
Step 10: Passwords, configuration and tuning
Set a new password for a Manager user with tomee-set-password. It stores the PBKDF2 hash, records the new password in /root/tomee-credentials.txt and restarts TomEE. With --generate it creates a random one:
sudo tomee-set-password admin --generate
Run sudo tomee-set-password deployer (without --generate) to type a password of your own (at least 16 characters) at the prompt; it never takes a password on the command line, so it stays out of your shell history and the system logs.
TomEE's configuration lives in /var/lib/tomee/conf (server.xml, tomee.xml, system.properties, context.xml). Resources such as data sources go in tomee.xml or your application's WEB-INF/resources.xml. The JVM options are set in the systemd unit; to change them (for example a fixed heap), add an override and restart:
sudo systemctl edit tomee
[Service]
Environment="CATALINA_OPTS=-server -Xms1g -Xmx4g -Djava.net.preferIPv4Stack=true -Djava.awt.headless=true"
sudo systemctl restart tomee
Uploads through nginx are limited to 512 MB per request by client_max_body_size in /etc/nginx/sites-available/cloudimg-tomee; change it there and reload nginx.
Step 11: Server components
| Component | Path |
|---|---|
| TomEE release (read only) | /opt/tomee |
| Configuration, applications, logs | /var/lib/tomee (conf, webapps, logs) on the data volume |
| Service unit | /etc/systemd/system/tomee.service |
| nginx site | /etc/nginx/sites-available/cloudimg-tomee |
| Certificate and key | /etc/ssl/tomee/ |
| First boot script | /usr/local/sbin/apache-tomee-firstboot.sh |
| Host name and password helpers | /usr/local/sbin/tomee-set-hostname, /usr/local/sbin/tomee-set-password |
| Credentials | /root/tomee-credentials.txt (mode 0600) |
sudo JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 /opt/tomee/bin/version.sh 2>/dev/null | grep -E 'Server (version|number)|JVM Version'
sudo journalctl -u tomee --no-pager -o cat | grep 'Server startup in' | tail -1
Server version: Apache Tomcat (TomEE)/10.0.27 (9.1.3)
Server number: 10.0.27.0
JVM Version: 17.0.20.1+1-1-24.04-Ubuntu
19-Oct-2026 18:47:46.198 INFO [main] jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke Server startup in [1746] milliseconds
TomEE's own logs are in /var/lib/tomee/logs (catalina.<date>.log, localhost_access_log.<date>.txt); nginx logs to /var/log/nginx.
Step 12: Security recommendations
- Allow ports 22, 80 and 443 in the security group from your own networks only, and consider limiting
/managerto your own addresses as well (anallow/denylist in the/managerlocation of the nginx site). - Keep the per instance passwords secret, and rotate them if they may have been shared (Step 10).
- Use your own DNS name and a certificate your clients trust (Step 9).
- Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else. TomEE 9.1.3 is the last 9.x release, so plan new applications on a current TomEE line. - Snapshot the
/var/lib/tomeedata volume to back up your applications and configuration.
Support and licensing
Apache TomEE is open source under the Apache License 2.0; there is no per user or per CPU licence fee. The AMI price covers cloudimg's hardening, first boot setup and support. Apache TomEE, Apache Tomcat and Apache are trademarks of the Apache Software Foundation; cloudimg is not affiliated with or endorsed by the Apache Software Foundation.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA