WildFly 40 on Ubuntu 24.04 LTS on Azure User Guide
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.shandjboss-cli.shboth work) -
wildfly.servicerunning as the unprivilegedwildflyuser with thestandalone-full.xmlprofile (web, EJB, JMS, JPA and web services) -
nginx reverse proxy serving the application on port 80
-
wildfly-firstboot.serviceprovisioning 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
- An active Azure subscription
- A subscription to the WildFly 40 listing in the Azure Marketplace
- An SSH key pair
- 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.

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

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>/:

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 Runtime view reports live JVM metrics for the server, including heap use against the tuned 1024 MB maximum:

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"}

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:

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

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.servicecarries aConditionPathExistsguard 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:9990and 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

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