Application Servers Azure

WildFly 40 on Ubuntu 24.04 LTS on Azure User Guide

| Product: WildFly 40 on Ubuntu 24.04 LTS

Overview

This guide covers deploying WildFly 40 on Ubuntu 24.04 LTS on Azure. WildFly is the upstream open source Jakarta EE application server, the engine behind Red Hat's commercial JBoss EAP. Version 40 implements Jakarta EE 11 and runs on OpenJDK 21.

The image installs WildFly 40.0.1.Final from the official GitHub release tarball, verified against the published SHA 256 checksum. On the first boot of every instance, wildfly-firstboot.service generates fresh passwords for both built in security realms, starts the server and writes the credentials to a root only file.

What is included:

  • WildFly 40.0.1.Final from the official GitHub release tarball, SHA 256 verified

  • OpenJDK 21 (full JDK, so add-user.sh and jboss-cli.sh both work)

  • wildfly.service running as the unprivileged wildfly user with the standalone-full.xml profile (web, EJB, JMS, JPA and web services)

  • nginx reverse proxy serving the application on port 80

  • wildfly-firstboot.service provisioning unique per instance credentials

  • JVM heap tuned for a 4 GB VM (256 MB initial, 1024 MB maximum)

  • Ubuntu 24.04 LTS base, fully patched, with unattended security upgrades enabled

  • 24/7 cloudimg support with a 24 hour response SLA

Key facts:

Item Value
Default login user azureuser
Application URL http://<vm-ip>/ (nginx on port 80)
Management console http://127.0.0.1:9990/console (loopback only, reached over an SSH tunnel)
Credentials file /stage/scripts/wildfly-credentials.log (mode 0600, root only)
JBOSS_HOME /opt/wildfly
Licence Apache License 2.0

Prerequisites

  1. An active Azure subscription
  2. A subscription to the WildFly 40 listing in the Azure Marketplace
  3. An SSH key pair
  4. A virtual network and subnet in your target region

Recommended VM size: Standard_B2s (2 vCPU, 4 GB RAM) for development and test. For production use Standard_D4s_v5 (4 vCPU, 16 GB RAM) and raise the heap as described under Tuning the JVM heap.

Step 1: Deploy from the Azure Portal

Search for WildFly 40 in the Azure Marketplace, select the cloudimg offering and click Create. Configure the network security group to allow TCP 22 for SSH and TCP 80 for HTTP from your own client networks.

You do not need to open port 9990. The management console deliberately binds to the loopback interface only and is reached over an SSH tunnel, so it is never exposed to the internet.

Step 2: Deploy from the Azure CLI

RG="wildfly-prod"; LOCATION="eastus"; VM_NAME="wildfly-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/wildfly-40-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 wf-vnet --address-prefix 10.101.0.0/16 \
  --subnet-name wf-subnet --subnet-prefix 10.101.1.0/24
az network nsg create -g "$RG" --name wf-nsg
az network nsg rule create -g "$RG" --nsg-name wf-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 wf-nsg --name allow-http --priority 110 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 80 --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 wf-vnet --subnet wf-subnet --nsg wf-nsg --public-ip-sku Standard

Step 3: Connect via SSH

ssh azureuser@<vm-ip>

Both wildfly.service and nginx.service start automatically once first boot initialisation has finished. On a Standard_B2s this takes roughly 30 to 60 seconds from power on.

Step 4: Verify the Services

sudo systemctl status wildfly.service nginx.service --no-pager

Both units report active (running). The WildFly unit shows the standalone-full.xml profile and the tuned heap flags on its command line.

wildfly.service and nginx.service both active and running, showing the standalone-full profile and the tuned JVM heap flags

Confirm first boot initialisation completed and check the listeners:

sudo test -f /var/lib/cloudimg/wildfly-firstboot.done && echo FIRSTBOOT_DONE
sudo ss -tln | grep -E ':(80|8080|9990) '

Expected output:

FIRSTBOOT_DONE
LISTEN 0      4096         0.0.0.0:8080      0.0.0.0:*
LISTEN 0      50         127.0.0.1:9990      0.0.0.0:*
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*
LISTEN 0      511             [::]:80           [::]:*

Note that port 9990 is bound to 127.0.0.1 only. That is the management interface and it is intentionally not reachable from outside the VM.

Step 5: Retrieve the Per Instance Credentials

Every instance generates its own credentials on first boot. Nothing is shared between deployments and no default password ships in the image.

sudo cat /stage/scripts/wildfly-credentials.log
# WildFly 40 - Per-VM Credentials
WILDFLY_URL=http://10.0.0.23/
WILDFLY_CONSOLE_URL=http://127.0.0.1:9990/console
WILDFLY_ADMIN_USER=admin
WILDFLY_ADMIN_PASSWORD=<WILDFLY_ADMIN_PASSWORD>
WILDFLY_APP_USER=appuser
WILDFLY_APP_PASSWORD=<WILDFLY_APP_PASSWORD>
HTTP_PORT=80
MGMT_PORT=9990
JBOSS_HOME=/opt/wildfly

The first boot sentinel and the per instance credentials file listing the admin and appuser accounts, with passwords redacted for this screenshot

There are exactly two accounts:

Account Realm Purpose
admin ManagementRealm Signs in to the management console and the management API
appuser ApplicationRealm Available to applications that use container managed security

Step 6: Open the Application

The application is served by nginx on port 80, so no tunnel is needed:

curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1/

From your workstation, browse to http://<vm-ip>/:

The WildFly welcome page served through nginx on port 80, confirming the instance is running

Step 7: Reach the Management Console

The management console binds to the loopback interface only. Open an SSH tunnel from your workstation:

ssh -L 9990:127.0.0.1:9990 azureuser@<vm-ip>

Leave that session open and browse to http://127.0.0.1:9990/console. Sign in as admin with the password from the credentials file:

The WildFly Management Console homepage signed in as admin, showing the Deployments, Configuration, Runtime and Access Control sections

The Runtime view reports live JVM metrics for the server, including heap use against the tuned 1024 MB maximum:

The Runtime Status view showing OpenJDK 21, server uptime, and live heap, non heap and thread usage graphs

Step 8: Query the Management API

The same operations are available over HTTP digest authentication on the loopback interface:

PASS=$(sudo grep '^WILDFLY_ADMIN_PASSWORD=' /stage/scripts/wildfly-credentials.log | cut -d= -f2-)

curl -s --digest -u "admin:${PASS}" -H 'Content-Type: application/json' \
  -d '{"operation":"read-attribute","name":"product-version"}' \
  http://127.0.0.1:9990/management

curl -s --digest -u "admin:${PASS}" -H 'Content-Type: application/json' \
  -d '{"operation":"whoami"}' http://127.0.0.1:9990/management

curl -s --digest -u "admin:${PASS}" -H 'Content-Type: application/json' \
  -d '{"operation":"read-attribute","name":"server-state"}' \
  http://127.0.0.1:9990/management

Expected output:

{"outcome" : "success", "result" : "40.0.1.Final"}
{"outcome" : "success", "result" : {"identity" : {"username" : "admin"}}}
{"outcome" : "success", "result" : "running"}

The management API returning product version 40.0.1.Final, the authenticated admin identity, and a running server state

Step 9: Deploy Your First Application

WildFly watches /opt/wildfly/standalone/deployments/ and deploys any archive dropped into it. The block below builds a small Jakarta EE web application with jar from the bundled JDK, deploys it, confirms it is being served, then removes it again so the server is left exactly as it was.

WORK=$(mktemp -d); mkdir -p "$WORK/hello/WEB-INF"
cat > "$WORK/hello/index.jsp" <<'JSP'
<html><body>
<h1>Hello from WildFly</h1>
<p>Served by: <%= application.getServerInfo() %></p>
</body></html>
JSP
cat > "$WORK/hello/WEB-INF/web.xml" <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" version="6.0">
  <welcome-file-list><welcome-file>index.jsp</welcome-file></welcome-file-list>
</web-app>
XML
jar -cf "$WORK/hello.war" -C "$WORK/hello" .
sudo install -o wildfly -g wildfly -m 0644 "$WORK/hello.war" /opt/wildfly/standalone/deployments/hello.war
for i in $(seq 1 90); do
  sudo test -f /opt/wildfly/standalone/deployments/hello.war.deployed && break
  sleep 1
done
curl -s http://127.0.0.1/hello/
sudo rm -f /opt/wildfly/standalone/deployments/hello.war
sleep 5
sudo rm -f /opt/wildfly/standalone/deployments/hello.war.*
rm -rf "$WORK"

The second removal clears the .undeployed marker that the deployment scanner writes once it notices the archive has gone.

Expected output:

<html><body>
<h1>Hello from WildFly</h1>
<p>Served by: WildFly 40.0.1.Final (WildFly Core 32.0.2.Final) - 2.4.1.Final</p>
</body></html>

While the archive is deployed it appears in the console under Deployments:

The Deployments view of the management console listing the deployed hello.war archive

And the application renders in the browser, with the server information string produced by WildFly itself:

The deployed Jakarta EE application rendering in the browser and reporting that it is served by WildFly 40.0.1.Final

For production deployments prefer the management API, which is transactional and reports failures. Substitute the path to your own archive for myapp.war:

PASS=$(sudo grep '^WILDFLY_ADMIN_PASSWORD=' /stage/scripts/wildfly-credentials.log | cut -d= -f2-)
curl --digest -u "admin:${PASS}" -F "file=@myapp.war" \
  http://127.0.0.1:9990/management-upload

Step 10: Server Components

Component Path
JBOSS_HOME /opt/wildfly
Launch script /opt/wildfly/bin/standalone.sh
Active configuration /opt/wildfly/standalone/configuration/standalone-full.xml
Management realm users /opt/wildfly/standalone/configuration/mgmt-users.properties
Application realm users /opt/wildfly/standalone/configuration/application-users.properties
Deployments directory /opt/wildfly/standalone/deployments/
Server log /opt/wildfly/standalone/log/server.log
WildFly unit /etc/systemd/system/wildfly.service
First boot script /usr/local/sbin/wildfly-firstboot.sh
First boot sentinel /var/lib/cloudimg/wildfly-firstboot.done
Bootstrap marker /var/lib/cloudimg/wildfly-bootstrap-ready
Credentials file /stage/scripts/wildfly-credentials.log
nginx site /etc/nginx/sites-available/cloudimg-wildfly

Confirm the Java runtime:

java -version

Expected output:

openjdk version "21.0.11" 2026-04-21
OpenJDK Runtime Environment (build 21.0.11+10-1-24.04.2-Ubuntu)
OpenJDK 64-Bit Server VM (build 21.0.11+10-1-24.04.2-Ubuntu, mixed mode, sharing)

Step 11: Managing the Services

sudo systemctl status wildfly.service --no-pager | head -5
sudo tail -n 20 /opt/wildfly/standalone/log/server.log

To restart or stop the server use sudo systemctl restart wildfly.service and sudo systemctl stop wildfly.service. nginx is managed the same way with sudo systemctl restart nginx.service.

Step 12: Tuning the JVM Heap

The image ships with a heap sized for a 4 GB VM. On a larger VM, raise it by editing the JAVA_OPTS line in /etc/systemd/system/wildfly.service:

Environment="JAVA_OPTS=-Xms2g -Xmx4g -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m"

Then reload and restart with sudo systemctl daemon-reload followed by sudo systemctl restart wildfly.service. As a rule of thumb, keep the maximum heap at or below half the VM memory so the operating system and nginx keep plenty of headroom.

Step 13: How the Security Model Works

This image is built so that no shared or default credential ever exists on a running instance.

  • No account ships in the image. WildFly's realm files are emptied before capture, and the build never creates a management user. There is no vendor default password to find.
  • The server cannot start unbootstrapped. wildfly.service carries a ConditionPathExists guard on /var/lib/cloudimg/wildfly-bootstrap-ready. First boot writes that marker only after both realm passwords have been provisioned, so systemd will not start WildFly before it has its own credentials.
  • The management interface is not published. It binds to 127.0.0.1:9990 and is reached over an SSH tunnel.

You can verify all of this on your own instance:

curl -s -o /dev/null -w 'unauthenticated -> HTTP %{http_code}\n' \
  -H 'Content-Type: application/json' -d '{"operation":"whoami"}' \
  http://127.0.0.1:9990/management
for P in admin:admin admin:password admin:wildfly jboss:jboss; do
  C=$(curl -s -o /dev/null -w '%{http_code}' --digest -u "$P" \
      -H 'Content-Type: application/json' -d '{"operation":"whoami"}' \
      http://127.0.0.1:9990/management)
  echo "  $P -> HTTP $C"
done

Expected output:

unauthenticated -> HTTP 401
  admin:admin -> HTTP 401
  admin:password -> HTTP 401
  admin:wildfly -> HTTP 401
  jboss:jboss -> HTTP 401

The management port bound to loopback only, an unauthenticated request rejected with 401, and every common default credential rejected with 401

Step 14: Security Recommendations

  • Rotate the admin password with sudo -u wildfly /opt/wildfly/bin/add-user.sh --silent --user admin --password '<new-password>' then restart the service
  • Restrict the network security group so port 80 is reachable only from networks you trust
  • Terminate TLS in front of the VM using Azure Application Gateway, or add a certificate to the nginx site for direct HTTPS
  • Keep the management interface on loopback. If you must reach it another way, use the SSH tunnel rather than rebinding it to a public address
  • Apply operating system updates monthly. Unattended security upgrades are enabled by default

Step 15: Troubleshooting

The application returns 502 from nginx. WildFly is still starting or is not running. Check sudo systemctl status wildfly.service and sudo tail -n 50 /opt/wildfly/standalone/log/server.log.

WildFly is inactive and will not start. Confirm the bootstrap marker exists with sudo test -f /var/lib/cloudimg/wildfly-bootstrap-ready && echo present. If it is missing, first boot did not finish. Inspect it with sudo journalctl -u wildfly-firstboot.service --no-pager.

The credentials file only contains a comment. First boot has not completed yet. Wait for /var/lib/cloudimg/wildfly-firstboot.done to appear, then read the file again.

The management console asks for a password repeatedly. The console uses HTTP digest authentication. Confirm you are using the admin password from the credentials file on this specific VM, since it is unique per instance.

A deployment does not activate. Check for a .failed marker beside your archive in /opt/wildfly/standalone/deployments/ and read server.log for the deployment error.

Step 16: Support and Licensing

WildFly is licensed under the Apache License 2.0, with no per CPU or per deployment fees. cloudimg provides commercial support for this image separately.

  • Email: support@cloudimg.co.uk
  • Website: www.cloudimg.co.uk
  • Support hours: 24/7 with a 24 hour response SLA