Open Integration Engine on Ubuntu 24.04 on Azure User Guide
Open Integration Engine on Ubuntu 24.04 LTS on Azure
This guide walks you through deploying the cloudimg Open Integration Engine image on Microsoft Azure, signing in with the admin password generated uniquely for your VM, and pushing your first message through a channel.
Open Integration Engine (OIE) is a community governed, MPL-2.0 licensed integration engine for healthcare interoperability. You build channels that receive, filter, transform and route messages between systems, with first class support for HL7 v2, FHIR, DICOM, X12, XML, JSON and delimited formats, plus JavaScript transformers and connectors for HTTP, TCP/MLLP, files, databases and JMS.
This image ships version 4.6.0.
What the image gives you
| Engine | Open Integration Engine 4.6.0 on OpenJDK 17 |
| Base OS | Ubuntu 24.04 LTS, fully patched at build time |
| Web edge | nginx on port 443 (TLS) in front of the engine; port 80 serves a health endpoint and redirects to HTTPS |
| Admin credential | Generated per VM on first boot. No shared or default password ships in the image |
| Data | Embedded Derby database, keystore and configuration on a dedicated 20 GiB Azure data disk |
| Self test | oie-engine-selftest.sh proves the engine really processes a message |
| Recommended size | Standard_B2s (2 vCPU / 4 GiB) for evaluation and light workloads |
Compliance note. This image is infrastructure. You remain responsible for every HIPAA, PHI and GDPR control that applies to your deployment, including your own domain with a trusted TLS certificate, network isolation, access control, audit logging and encryption at rest. Nothing here is certified compliant.
1. Launch the VM
From the Azure portal
- Open the offer in the Azure Marketplace and choose Get It Now, then Create.
- Pick your subscription, resource group and region.
- Choose a VM size.
Standard_B2sis enough to evaluate the engine and run light channel traffic; scale up for production throughput. - Set the administrative username to
azureuserand supply your SSH public key. - On the Networking tab allow inbound 22 (SSH) and 443 (HTTPS). Port 80 is optional and only serves a health endpoint plus a redirect to HTTPS.
- Create the VM.
From the Azure CLI
These commands run on your own workstation, not on the VM:
az vm create \
--resource-group my-resource-group \
--name my-oie-vm \
--image <offer-urn-from-the-marketplace-listing> \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
# Then open only the ports you need, restricted to the networks that should reach them.
az vm open-port --resource-group my-resource-group --name my-oie-vm \
--port 443 --priority 1010 --source-address-prefixes <your-mgmt-cidr>
az vm open-port --resource-group my-resource-group --name my-oie-vm \
--port 22 --priority 1020 --source-address-prefixes <your-mgmt-cidr>
First boot takes about a minute: the engine creates a fresh database, rotates the admin password to a value unique to your VM, generates a TLS certificate for this VM, and only then starts nginx.
2. Retrieve your per-VM admin password
The image ships no usable credential. The password is generated on your VM at first boot and written to a root-only file.
ssh azureuser@<vm-ip>
sudo cat /root/oie-credentials.txt
# Open Integration Engine - generated on first boot by oie-firstboot.service
# These credentials are unique to this VM. Store them somewhere safe.
OIE_URL=https://<vm-ip>/
OIE_API_BASE=https://<vm-ip>/api
OIE_USERNAME=admin
OIE_PASSWORD=<OIE_PASSWORD>
Copy the password into your password manager, then confirm the services are healthy:
systemctl status oie nginx --no-pager --lines=5

You can see exactly what first boot did, including the credential rotation and the proof that the old default login no longer works:
sudo journalctl -u oie-firstboot.service --no-pager | tail -14

3. Open the web interface
Browse to https://<vm-public-ip>/.
The appliance uses a self-signed certificate generated for your VM, so your browser will warn you the first time. Accept the warning for evaluation, and see section 8 before you go to production.

This landing page confirms the backend is running and gives you the URL to point an Administrator Launcher at. Two things are worth understanding here:
- Channel authoring happens in the Administrator, a desktop application you download separately from the project. Point it at the URL shown on this page and sign in as
adminwith your per-VM password. - Everything the Administrator does is also available over the REST API, which is fully browsable on this VM. That is the path this guide uses, because it works from any machine with
curland needs nothing installed.
4. Explore the Client API
Click Go to API, or browse to https://<vm-public-ip>/api/.

Expanding Channels shows the operations you use to create, deploy, start, stop and inspect channels.

One API rule to remember. The engine ships with
server.api.require-requested-with = true, so every REST call must carry anX-Requested-Withheader. The API explorer adds it for you. Fromcurlyou must add it yourself, as shown below. Without it the engine replies400 All requests must have 'X-Requested-With' header.
Sign in from the command line and keep the session cookie:
PW=$(sudo grep '^OIE_PASSWORD=' /root/oie-credentials.txt | cut -d= -f2-)
curl -sk -c /tmp/oie-cookies -H 'X-Requested-With: XMLHttpRequest' \
--data-urlencode 'username=admin' \
--data-urlencode "password=${PW}" \
https://127.0.0.1/api/users/_login
A successful sign in returns a LoginStatus of SUCCESS. Check the engine version the same way:
curl -sk -b /tmp/oie-cookies -H 'X-Requested-With: XMLHttpRequest' \
https://127.0.0.1/api/server/version
4.6.0
5. Prove the engine works, in one command
A web page returning 200 tells you a web server is up. It does not tell you the engine can actually move a message. The image ships a self test that answers the real question: it deploys a temporary channel, pushes an HL7 v2 message through it, checks the engine's own statistics, and removes the channel again.
sudo /usr/local/sbin/oie-engine-selftest.sh

received=1 sent=1 error=0 means the message travelled through the source connector, the transformer and the destination connector. Run this any time you want to confirm the engine is healthy, including after you change configuration.
6. Deploy your first channel
The image ships the self test's channel definition at /usr/share/cloudimg/oie/example-channel.xml so you can run this section verbatim. It is a minimal Channel Reader source feeding a JavaScript Writer destination.
Give it an id and a name of your own:
CHANNEL_ID=$(cat /proc/sys/kernel/random/uuid)
echo "${CHANNEL_ID}" | sudo tee /tmp/oie-channel-id >/dev/null
sudo sed -e "s/CLOUDIMG_CHANNEL_ID/${CHANNEL_ID}/" \
-e "s/CLOUDIMG_CHANNEL_NAME/my-first-channel/" \
/usr/share/cloudimg/oie/example-channel.xml | sudo tee /tmp/my-channel.xml >/dev/null
echo "prepared channel ${CHANNEL_ID}"
Create it:
curl -sk -b /tmp/oie-cookies -H 'X-Requested-With: XMLHttpRequest' \
-H 'Content-Type: application/xml' \
--data-binary @/tmp/my-channel.xml \
-o /dev/null -w 'create: HTTP %{http_code}\n' \
https://127.0.0.1/api/channels
Deploy it:
CHANNEL_ID=$(cat /tmp/oie-channel-id)
curl -sk -b /tmp/oie-cookies -H 'X-Requested-With: XMLHttpRequest' \
-H 'Content-Type: application/xml' \
--data "<set><string>${CHANNEL_ID}</string></set>" \
-o /dev/null -w 'deploy: HTTP %{http_code}\n' \
"https://127.0.0.1/api/channels/_deploy?returnErrors=true"
Confirm it reached STARTED:
CHANNEL_ID=$(cat /tmp/oie-channel-id)
for i in $(seq 1 30); do
STATE=$(curl -sk -b /tmp/oie-cookies -H 'X-Requested-With: XMLHttpRequest' \
"https://127.0.0.1/api/channels/${CHANNEL_ID}/status" | grep -oE '<state>[A-Z]+</state>')
echo "${STATE}"
case "${STATE}" in *STARTED*) break;; esac
sleep 2
done
Push an HL7 v2 message through it:
CHANNEL_ID=$(cat /tmp/oie-channel-id)
curl -sk -b /tmp/oie-cookies -H 'X-Requested-With: XMLHttpRequest' \
-H 'Content-Type: text/plain' \
--data-binary 'MSH|^~\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20260101000000||ADT^A01|MSG00001|P|2.5' \
-w '\nsubmit: HTTP %{http_code}\n' \
"https://127.0.0.1/api/channels/${CHANNEL_ID}/messages?destinationMetaDataId=1"
Do not omit
destinationMetaDataId. It names the destination connectors the message should be routed to. Without it the engine accepts the message and runs the source connector only: statistics showreceived=1andsent=0forever, and your destinations never fire. This is the single most common surprise when driving the engine from the REST API.
Check the statistics. received and sent should both be 1:
CHANNEL_ID=$(cat /tmp/oie-channel-id)
curl -sk -b /tmp/oie-cookies -H 'X-Requested-With: XMLHttpRequest' \
"https://127.0.0.1/api/channels/${CHANNEL_ID}/statistics"

When you are finished with the example, undeploy and remove it:
CHANNEL_ID=$(cat /tmp/oie-channel-id)
curl -sk -b /tmp/oie-cookies -H 'X-Requested-With: XMLHttpRequest' \
-H 'Content-Type: application/xml' \
--data "<set><string>${CHANNEL_ID}</string></set>" \
-o /dev/null -w 'undeploy: HTTP %{http_code}\n' \
https://127.0.0.1/api/channels/_undeploy
curl -sk -b /tmp/oie-cookies -H 'X-Requested-With: XMLHttpRequest' \
-X DELETE -o /dev/null -w 'delete: HTTP %{http_code}\n' \
"https://127.0.0.1/api/channels/${CHANNEL_ID}"
To build real channels, export a definition from the Administrator and create it the same way.
7. Opening ports for your own channel listeners
The image opens only what the appliance itself needs: 22, 80 and 443.
Listener ports belong to the channels you create. An MLLP listener on 6661, an HTTP listener on 8081 or a TCP listener on any port will bind on the VM as soon as you deploy the channel, but Azure will not admit traffic to it until you open that port too:
az vm open-port --resource-group my-resource-group --name my-oie-vm \
--port 6661 --priority 1030 --source-address-prefixes <your-mgmt-cidr>
Open only the ports you actually use, and prefer restricting the source address range on the NSG rule to the systems that are meant to talk to the engine.
8. Security posture and hardening
The image is secure by default in three specific ways.
No known credential ships in the image. The engine's database is wiped before the image is captured, so the captured image contains no account at all. Your VM creates the database fresh on first boot and rotates the admin password to a value unique to your VM before the public web edge is started.
The engine is not directly exposed. Its admin and REST listeners bind to 127.0.0.1 only. nginx on ports 80 and 443 is the sole public edge.
sudo ss -tlnp | grep -E ':8443|:8080|:443|:80 '

The old default login is provably dead. You can check this yourself at any time:
curl -sk -H 'X-Requested-With: XMLHttpRequest' \
--data-urlencode 'username=admin' --data-urlencode 'password=admin' \
https://127.0.0.1/api/users/_login
This returns <status>FAIL</status>. If it ever returns SUCCESS, stop and rebuild the VM.
Before production
- Replace the TLS certificate. Point a DNS name at the VM and install a trusted certificate:
bash
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d oie.example.com
- Restrict access. Limit inbound 443 to the networks that need the Administrator or API, using an NSG rule or a private endpoint.
- Change the admin password to one of your own and create named per-person accounts rather than sharing
admin. - Back up the data disk. Everything stateful lives on it, so an Azure snapshot schedule against that disk captures your channels and message history.
9. Where everything lives
| Path | Contents |
|---|---|
/opt/oie |
Engine installation, launcher, extensions and logs |
/opt/oie/conf/mirth.properties |
Engine configuration, including the loopback bind and data directory |
/opt/oie/conf/custom.vmoptions |
JVM options, including the heap size |
/var/lib/oie/appdata |
Data disk. Derby database, keystore, configuration map and temporary data |
/root/oie-credentials.txt |
Your per-VM admin credentials, root readable only |
/usr/local/sbin/oie-engine-selftest.sh |
The engine self test |
Service management:
sudo systemctl status oie
sudo systemctl restart oie
sudo journalctl -u oie -f
Tuning the JVM heap
The image sets a 1 GiB maximum heap, which suits Standard_B2s. If you move to a larger VM and drive real message volume, raise it:
sudo sed -i 's/^-Xmx1024m/-Xmx3072m/' /opt/oie/conf/custom.vmoptions
sudo systemctl restart oie
Leave at least a gigabyte for the operating system, nginx and the database cache. Never add a swap file to compensate for an undersized VM; move to a larger size instead.
10. Troubleshooting
The web page does not load. Confirm nginx is running and port 443 is open in the NSG:
sudo systemctl status nginx --no-pager
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/healthz
/healthz returns 200 from the VM itself whenever the edge is healthy.
The credentials file still shows the placeholder. First boot has not finished. Watch it:
sudo journalctl -u oie-firstboot.service -f
The engine's first start creates the database and can take up to a couple of minutes.
A REST call returns 400. You omitted the X-Requested-With header. See section 4.
A channel receives messages but never sends them. You omitted destinationMetaDataId when submitting the message. See section 6.
The engine will not start after a configuration change. Check the log:
sudo journalctl -u oie --no-pager | tail -50
sudo tail -50 /opt/oie/logs/mirth.log
Support
This image is maintained by cloudimg with 24/7 support for deployment, upgrades, TLS setup and channel configuration questions. Contact support@cloudimg.co.uk.
Open Integration Engine itself is open source under the Mozilla Public License 2.0. Project documentation, the Administrator Launcher downloads and the community are at openintegrationengine.org.