Eclipse Jetty 12 on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of Eclipse Jetty 12 on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Jetty is a lightweight, embeddable Java HTTP server and Jakarta Servlet/JSP container — the runtime teams reach for when Tomcat or WildFly are heavier than a workload needs, and a common embedded app server behind Java microservices.
The image installs the official jetty-home 12.0.37 distribution (Jakarta EE10 / Servlet 6.0 environment) from Maven Central, pinned by exact version and verified against a sha256 checksum. It runs on OpenJDK 21 (Jetty 12 requires Java 17+). Because Jetty is a web application server, this image ships a genuinely browsable web UI: a cloudimg-branded landing page at the server root plus Jetty's own standard demo web applications pre-deployed, so you can point a browser at the VM and click through real, working example apps — a Hello-World servlet, request/session/cookie dump servlets, WebSocket and Comet chat demos, and JSP/JSTL examples.
What is included:
-
Eclipse Jetty 12.0.37 (
jetty-homedistribution) from the official Maven Central release, sha256-pinned -
OpenJDK 21 JRE headless (Jetty 12 requires Java 17+)
-
jetty.servicesystemd unit auto-starting on boot, running as the unprivilegedjetty:jettysystem user -
eclipse-jetty-firstboot.servicesystemd oneshot that starts Jetty + nginx and confirms the demo webapps answer before completing -
HTTP connector bound to loopback only (
127.0.0.1:8080) — nginx fronts it on:80 -
Unauthenticated
/healthzendpoint (nginx-native, HTTP 200) for load balancer / probe checks -
A cloudimg-branded root landing page at context
/that renders a live server timestamp and request counter (proof the JSP container executes per request) and links out to the demo apps -
Jetty's own demo web applications pre-deployed: the flagship demo webapp at
/ee10-test/and the JSP/JSTL demo webapp at/ee10-demo-jsp/ -
JETTY_BASE=/var/lib/jettyon a dedicated 20 GiB Azure data disk — webapps, logs and any deployed application state survive independently of the OS disk -
Ubuntu 24.04 LTS base with latest security patches applied at build time
-
Azure Linux Agent for seamless cloud integration and SSH key injection
-
24/7 cloudimg support with guaranteed 24 hour response SLA
Prerequisites
-
Active Azure subscription, SSH public key, VNet + subnet in target region
-
Subscription to the Eclipse Jetty 12 listing on Azure Marketplace
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for development. Production Servlet workloads should use Standard_D4s_v5 (4 vCPU, 16 GB RAM) or larger.
Step 1: Deploy from the Azure Portal
Search Eclipse Jetty 12 in Marketplace, select the cloudimg publisher, click Create. NSG rules: TCP 22 (admin), TCP 80 (HTTP), TCP 443 (reserved for TLS) from your client networks.
Step 2: Deploy from the Azure CLI
RG="jetty-prod"; LOCATION="eastus"; VM_NAME="jetty-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/eclipse-jetty-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name jetty-vnet --address-prefix 10.100.0.0/16 --subnet-name jetty-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name jetty-nsg
az network nsg rule create -g "$RG" --nsg-name jetty-nsg --name allow-ssh --priority 100 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name jetty-nsg --name allow-http --priority 110 \
--destination-port-ranges 80 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name jetty-nsg --name allow-https --priority 120 \
--destination-port-ranges 443 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
--size Standard_B2s --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name jetty-vnet --subnet jetty-subnet --nsg jetty-nsg --public-ip-sku Standard
Step 3: Connect via SSH
ssh azureuser@<vm-ip>
jetty.service, nginx.service and eclipse-jetty-firstboot.service all start automatically on first boot — there is no admin account to set up, because Jetty has no login of its own.
Step 4: Verify the Jetty and nginx Services
sudo systemctl status jetty.service --no-pager
sudo systemctl status nginx.service --no-pager
sudo test -f /var/lib/cloudimg/eclipse-jetty-firstboot.done && echo FIRSTBOOT_DONE
sudo ss -tln | grep -E ':(80|8080) '
Expected output:
● jetty.service - Eclipse Jetty Servlet Container
Active: active (running)
Main PID: 3661 (java)
...Started ServerConnector@...{HTTP/1.1, (http/1.1)}{127.0.0.1:8080}
● nginx.service - A high performance web server and a reverse proxy server
Active: active (running)
FIRSTBOOT_DONE
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 50 [::ffff:127.0.0.1]:8080 *:*
LISTEN 0 511 [::]:80 [::]:*
Notice the HTTP connector is bound to 127.0.0.1:8080 only — Jetty is never directly reachable from the network; nginx on :80 is the only public entry point.

Step 5: Check the Health Endpoint
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/healthz
Expected output: 200. This unauthenticated endpoint is served directly by nginx (not proxied to Jetty) so it always answers even under Servlet-container load — use it for a load balancer or uptime probe.
Step 6: Open the Jetty Landing Page in a Browser
Point a browser at the VM's public address (http://<vm-ip>/, NSG must allow TCP 80). The image serves a cloudimg-branded landing page that is itself rendered by Jetty's JSP servlet container — the server timestamp and hit count are recomputed on every request, and the page links out to Jetty's demo web applications.

You can prove the per-request execution from the shell too — request the root twice and watch both values change:
curl -s http://127.0.0.1/ | grep -oE '(Server time:|Hit count \(this JVM\):)</strong>[^<]*'
curl -s http://127.0.0.1/ | grep -oE '(Server time:|Hit count \(this JVM\):)</strong>[^<]*'
Expected output — the timestamp and hit count both change on every request:
Server time:</strong> 2026-07-09T19:58:11.578448807Z
Hit count (this JVM):</strong> 1
Server time:</strong> 2026-07-09T19:58:12.031117662Z
Hit count (this JVM):</strong> 2
This proves the Servlet/JSP container is genuinely executing code on every request — a request-scoped counter and a live server timestamp cannot come from a cached or static file. This is the actual value a Servlet container adds over a plain web server like nginx or Apache HTTPD on its own.

Step 7: Explore Jetty's Demo Web Applications
The image pre-deploys Jetty's own standard demo webapps so you can see the container running real applications. Open them in a browser and click through the examples.
The flagship Jetty demo webapp is served at http://<vm-ip>/ee10-test/. It exercises a Hello-World servlet, request/session/cookie dump servlets, async responses, a Comet long-polling chat and WebSocket examples:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/ee10-test/
Expected output: 200.

Click the Request dump servlet (/ee10-test/dump/info) to see Jetty introspect the live HTTP request — method, headers, scheme, session id and more — all produced by a running servlet:

The JSP & JSTL demo webapp at http://<vm-ip>/ee10-demo-jsp/ shows the JSP engine compiling and executing pages on the fly (JSP dump, JSTL iteration, expression-language examples):
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/ee10-demo-jsp/
Expected output: 200.

Step 8: Confirm the Jetty Version and Deployed Webapps
head -1 /opt/jetty/VERSION.txt
ls -1 /var/lib/jetty/webapps/
Expected output:
12.0.37 - 27 June 2026
ee10-demo-jetty.d
ee10-demo-jetty.war
ee10-demo-jetty.xml
ee10-demo-jsp.war
root
The root directory maps to context path / (Jetty's convention, matching the demo-root module shipped in jetty-home itself); ee10-demo-jetty.war deploys at /ee10-test (see its ee10-demo-jetty.xml context descriptor) and ee10-demo-jsp.war at /ee10-demo-jsp.

Step 9: Read the Endpoint Notes
At first boot, eclipse-jetty-firstboot.service writes a non-secret notes file documenting the deployed URLs (Jetty has no credentials to rotate):
sudo cat /var/lib/cloudimg/eclipse-jetty-endpoints.notes
Expected output:
# Eclipse Jetty — Endpoint Notes
# Generated on first boot by eclipse-jetty-firstboot.service
#
# Jetty has no admin UI or login — there is nothing to authenticate to.
#
JETTY_DEMO_URL=http://<vm-ip>/
JETTY_DEMO_WEBAPP_URL=http://<vm-ip>/ee10-test/
JETTY_JSP_DEMO_URL=http://<vm-ip>/ee10-demo-jsp/
JETTY_HEALTHZ_URL=http://<vm-ip>/healthz
JETTY_HOME=/opt/jetty
JETTY_BASE=/var/lib/jetty
DEPLOY_YOUR_OWN_WAR=Drop a .war file into /var/lib/jetty/webapps/ — Jetty hot-deploys it.

Step 10: Deploy Your Own WAR
Jetty's ee10-deploy module watches $JETTY_BASE/webapps/ and hot-deploys any WAR file dropped into it — no restart required:
# Build your WAR (locally)
mvn package -DskipTests
# Copy to the VM
scp target/myapp.war azureuser@<vm-ip>:/tmp/
# Deploy (hot-deploy, no restart needed)
ssh azureuser@<vm-ip> 'sudo mv /tmp/myapp.war /var/lib/jetty/webapps/'
# Jetty auto-detects and deploys within a few seconds; visit:
# http://<vm-ip>/myapp/
To deploy at context path / instead (replacing the demo landing page), remove /var/lib/jetty/webapps/root/ first, then drop in a WAR or exploded directory named root. You can likewise remove the ee10-demo-jetty* and ee10-demo-jsp.war demo webapps from /var/lib/jetty/webapps/ once you no longer need them.
Step 11: Server Components
| Component | Path |
|---|---|
| JETTY_HOME (binaries, immutable) | /opt/jetty |
| JETTY_BASE (data disk) | /var/lib/jetty |
| Webapps | /var/lib/jetty/webapps/ |
| Root landing webapp | /var/lib/jetty/webapps/root/index.jsp |
Jetty demo webapp (/ee10-test) |
/var/lib/jetty/webapps/ee10-demo-jetty.war |
JSP demo webapp (/ee10-demo-jsp) |
/var/lib/jetty/webapps/ee10-demo-jsp.war |
| Start modules | /var/lib/jetty/start.d/ |
| Logs | systemd journal (journalctl -u jetty.service); /var/lib/jetty/logs/ if the requestlog module is enabled |
| Systemd unit | /etc/systemd/system/jetty.service |
| Firstboot script | /usr/local/sbin/eclipse-jetty-firstboot.sh |
| Firstboot service | /etc/systemd/system/eclipse-jetty-firstboot.service |
| Endpoint notes | /var/lib/cloudimg/eclipse-jetty-endpoints.notes (mode 0644) |
| Firstboot sentinel | /var/lib/cloudimg/eclipse-jetty-firstboot.done |
| nginx vhost | /etc/nginx/sites-available/cloudimg-jetty |
Step 12: Managing the Jetty Service
sudo systemctl status jetty.service --no-pager
sudo systemctl restart jetty.service
sudo journalctl -u jetty.service -n 50 --no-pager
By default Jetty's stdout/stderr go to the systemd journal (journalctl -u jetty.service), not a file under $JETTY_BASE/logs/. Use sudo journalctl -u jetty.service -f to follow the log live. To also write a request/access log to a file, enable Jetty's requestlog module (sudo -u jetty java -jar /opt/jetty/start.jar --add-modules=requestlog, from /var/lib/jetty), which writes to $JETTY_BASE/logs/.
Enabling additional Jetty modules (WebSocket, HTTP/2, SSL termination at Jetty itself, etc.) is a one-line --add-modules from $JETTY_BASE:
cd /var/lib/jetty
sudo -u jetty java -jar /opt/jetty/start.jar --add-modules=http2c
sudo systemctl restart jetty.service
Step 13: Security Recommendations
-
Never expose port 8080 directly — it is bound to loopback only by design; always go through nginx (
:80) or add a TLS-terminating reverse proxy on:443 -
Add TLS by fronting nginx with Let's Encrypt/Certbot, or terminate TLS in Jetty itself via the
sslmodule and a certificate on the data disk -
Remove the demo webapps before production if you do not need them — delete
ee10-demo-jetty*andee10-demo-jsp.warfrom/var/lib/jetty/webapps/. The bundled Jetty demo login realm is used only by the demo webapp's protected example pages and grants no OS access, but a lean production image should carry only your own application