Analytics Azure

Pathling FHIR Analytics Server on Ubuntu 24.04 on Azure User Guide

| Product: Pathling on Ubuntu 24.04 on Azure

Overview

This guide covers the deployment and use of Pathling on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images.

Pathling is an open source FHIR analytics server from CSIRO's Australian e-Health Research Centre. It exposes a standard HL7 FHIR R4 REST endpoint backed by an embedded, single node Apache Spark engine. Clinical resources such as Patient, Observation and Condition are stored in a local Delta warehouse, queried through the FHIR API, and — this is the point of Pathling — reshaped into flat, tabular datasets with SQL on FHIR ViewDefinitions, so analysts can run population level analytics over FHIR data without standing up a separate Spark cluster.

The image runs the official pinned ghcr.io/aehrc/pathling:2.0.1 container as a boot managed systemd service, with the Spark warehouse on the local disk and nginx in front of it. The server speaks the full FHIR REST surface — create, read, search, bulk $import — alongside the ViewDefinition/$run operation that materialises a view as CSV or NDJSON.

Security by design, no baked credential. Pathling itself ships with no authentication, and its write, import and admin surfaces mutate clinical data, so the cloudimg image puts nginx in front of the server and enforces a per VM HTTP Basic Auth credential on every path except the harmless read only capability statement GET /fhir/metadata, which stays public as a health and discovery surface. On first boot each VM generates a unique credential, writes it to the root only file /root/pathling-credentials.txt, and only then does the exposed surface accept a request. The container itself binds host loopback only, and a ufw firewall allows just SSH (22) and HTTP (80).

What is included:

  • Pathling Server 2.0.1 (FHIR R4 / 4.0.1) with embedded Apache Spark, running under systemd as pathling.service

  • nginx reverse proxy on port 80 enforcing per VM HTTP Basic Auth on all data, write, import and admin paths, with GET /fhir/metadata public

  • A local Delta warehouse for FHIR resources, empty on arrival and initialised on first container start

  • A per VM access credential generated on first boot and written to a root only credentials file

  • A ufw firewall (default deny inbound, allow 22 + 80) and no swap in the image

Deploying the VM

Launch the image from the Azure Marketplace as you would any other VM. The recommended size is Standard_B2ms (2 vCPU / 8 GiB). Pathling embeds a Spark JVM, so it needs more memory than a typical web app; the container's heap is capped at 4 GiB (JAVA_TOOL_OPTIONS=-Xmx4g) to fit this size with headroom. Larger workloads benefit from a bigger SKU — raise the heap cap in /etc/pathling-appliance/pathling.env in proportion if you move up.

Pathling is reached over TCP 80. The Azure network security group is your first line of defence: open 80 only to the client addresses or subnets that genuinely need it, never to the whole internet. The per VM Basic Auth credential is the second layer. Because Basic Auth sends credentials with each request, front the VM with TLS (an Azure Application Gateway or your own reverse proxy) before exposing it beyond a trusted network.

First boot

The first time the VM boots, pathling-firstboot.service runs once and then disables itself. It:

  1. resolves the VM's public IP for the info file,
  2. generates a unique HTTP Basic Auth password for the pathling user,
  3. writes it into the nginx credential file /etc/nginx/pathling.htpasswd,
  4. writes /root/pathling-credentials.txt (mode 600, owned by root),
  5. writes a message of the day pointer.

Only after first boot has written the credential do pathling.service and nginx.service start, so the exposed surface is never up without the credential in place. Nothing is required of you.

Retrieving your per VM credentials

The password is unique to your VM and is written in plain text only in this one root only file:

sudo cat /root/pathling-credentials.txt

Expected output (your host, and of course your password, will differ):

# Pathling FHIR analytics server — generated on first boot by pathling-firstboot.service.
# Access the server through nginx on the URL below. All data read/write, the
# $import operation, SQL-on-FHIR analytics, and the /admin console require these
# per-VM HTTP Basic Auth credentials. GET <url>/fhir/metadata is public.

PATHLING_URL=http://20.30.40.50
PATHLING_USER=pathling
PATHLING_PASSWORD=<unique to your VM>

Terminal showing the per VM Pathling credentials file at /root/pathling-credentials.txt with mode 600 owned by root, listing the server URL, the pathling Basic Auth user and the per VM password, alongside proof that the public capability statement returns 200 while an unauthenticated data read is rejected with 401

Copy the password into your secret store now. To rotate it, regenerate the nginx credential with sudo htpasswd -B /etc/nginx/pathling.htpasswd pathling, reload nginx with sudo systemctl reload nginx, and update the file to match.

Checking service health

The three services that make up the appliance are Docker (running the Pathling container), Pathling itself and nginx:

sudo systemctl is-active docker pathling nginx

Expected output:

active
active
active

The capability statement is public — no credential is needed — and is the recommended readiness probe. It names the FHIR version the server speaks and the Pathling build:

curl -s http://127.0.0.1/fhir/metadata | jq '{resourceType, fhirVersion, software}'
{
  "resourceType": "CapabilityStatement",
  "fhirVersion": "4.0.1",
  "software": {
    "name": "Pathling Server",
    "version": "2.0.1+78a3f75"
  }
}

fhirVersion 4.0.1 is FHIR R4, and the software block confirms the pinned Pathling Server release.

Terminal showing the Pathling appliance services docker, pathling and nginx all active under systemd, followed by the public FHIR capability statement returning resourceType CapabilityStatement, fhirVersion 4.0.1 and software Pathling Server version 2.0.1

The security model

Everything except the capability statement is protected by the per VM credential. You can see the model directly. An unauthenticated data read is rejected:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/fhir/Patient?_summary=count
401

The same request with the per VM credential succeeds. The commands below read the credential straight from the root only file, so they work on your VM without you typing the password:

sudo bash -c 'U=$(grep ^PATHLING_USER= /root/pathling-credentials.txt | cut -d= -f2); P=$(grep ^PATHLING_PASSWORD= /root/pathling-credentials.txt | cut -d= -f2); curl -s -u "$U:$P" "http://127.0.0.1/fhir/Patient?_summary=count" | jq "{resourceType, total}"'
{
  "resourceType": "Bundle",
  "total": 2
}

From your workstation, substitute your VM's public address and the password from the credentials file: curl -u pathling:<your-per-vm-password> http://<your-vm-public-ip>/fhir/Patient?_summary=count.

Loading and querying FHIR data

Create a resource with a standard FHIR POST. The server assigns an id and returns 201 Created with a Location header:

sudo bash -c 'U=$(grep ^PATHLING_USER= /root/pathling-credentials.txt | cut -d= -f2); P=$(grep ^PATHLING_PASSWORD= /root/pathling-credentials.txt | cut -d= -f2); curl -s -D - -o /dev/null -u "$U:$P" -X POST http://127.0.0.1/fhir/Patient -H "Content-Type: application/fhir+json" --data "{\"resourceType\":\"Patient\",\"gender\":\"female\",\"name\":[{\"family\":\"Demo\"}]}" | grep -iE "^HTTP|^location"'
HTTP/1.1 201
Location: http://127.0.0.1/fhir/Patient/f9ca32c1-9fa7-4f7e-86d4-3eba965fe662

Query it back with a standard FHIR search. _summary=count returns just the total in the Bundle:

sudo bash -c 'U=$(grep ^PATHLING_USER= /root/pathling-credentials.txt | cut -d= -f2); P=$(grep ^PATHLING_PASSWORD= /root/pathling-credentials.txt | cut -d= -f2); curl -s -u "$U:$P" "http://127.0.0.1/fhir/Patient?gender=female&_summary=count" | jq "{total}"'
{
  "total": 2
}

The full FHIR REST surface is available: GET /fhir/Patient/{id} to read a resource, PUT to update, DELETE to remove, and the asynchronous bulk POST /fhir/$import (with header Prefer: respond-async) to load NDJSON from the server's staging area for larger datasets.

Terminal showing an authenticated FHIR create returning HTTP 201 with a Location header for the new Patient, followed by a FHIR search with _summary=count returning a Bundle total, proving the write and query paths work end to end with the per VM credential

Running analytics with SQL on FHIR

The reason to run Pathling rather than a plain FHIR store is analytics. A ViewDefinition (from the SQL on FHIR v2 specification) describes how to flatten a FHIR resource into columns; the $run operation executes it over Spark and returns a tabular result as CSV or NDJSON.

This view flattens Patient into an id and a gender column and asks for CSV:

sudo bash -c 'U=$(grep ^PATHLING_USER= /root/pathling-credentials.txt | cut -d= -f2); P=$(grep ^PATHLING_PASSWORD= /root/pathling-credentials.txt | cut -d= -f2); VIEW="{\"resourceType\":\"Parameters\",\"parameter\":[{\"name\":\"viewResource\",\"resource\":{\"resourceType\":\"ViewDefinition\",\"name\":\"pb\",\"status\":\"active\",\"resource\":\"Patient\",\"select\":[{\"column\":[{\"name\":\"id\",\"path\":\"getResourceKey()\"},{\"name\":\"gender\",\"path\":\"gender\"}]}]}},{\"name\":\"_format\",\"valueString\":\"text/csv\"}]}"; curl -s -u "$U:$P" -X POST "http://127.0.0.1/fhir/ViewDefinition/\$run" -H "Content-Type: application/fhir+json" -H "Accept: text/csv" --data "$VIEW"'
id,gender
Patient/f9ca32c1-9fa7-4f7e-86d4-3eba965fe662,female
Patient/aebbb420-7743-47d8-9a60-2ab7fbb1d8b1,female
Patient/93162a59-9891-4724-a623-250b943279f4,male

Each select.column entry is a FHIRPath expression, so views can reach into nested elements, unnest repeating fields with forEach, and pull coded values out of CodeableConcept. Because $run executes on Spark, the same view definition scales from a handful of resources to millions. Point the CSV or NDJSON output straight at your analytics notebook, a data warehouse load, or a BI tool.

Terminal showing a SQL on FHIR ViewDefinition posted to the ViewDefinition run operation and the CSV response with an id and gender column and one row per Patient, demonstrating Pathling materialising a flat tabular dataset over embedded Spark

The admin console

Pathling also serves a browsable admin console at /admin/ for importing data, browsing resources and managing ViewDefinitions interactively. It sits behind the same per VM credential — open http://<your-vm-public-ip>/admin/ in a browser and authenticate with user pathling and the password from your credentials file.

Where things live

Path Purpose
/var/lib/pathling/warehouse Delta warehouse holding the FHIR resources
/var/lib/pathling/staging Staging area for NDJSON $import sources
/etc/pathling-appliance/pathling.env Container configuration (warehouse path, Spark, heap cap)
/etc/nginx/pathling.htpasswd Per VM HTTP Basic Auth credential (nginx)
/root/pathling-credentials.txt Per VM credentials, mode 600, root only
/etc/systemd/system/pathling.service systemd unit running the container

Change the heap cap or Spark settings by editing /etc/pathling-appliance/pathling.env, then restart the container with sudo systemctl restart pathling.

Security checklist

  • Restrict the NSG. Allow port 80 only from the subnets or addresses that need it.
  • Terminate TLS in front of the VM. Basic Auth sends the credential with every request, so place an Azure Application Gateway or your own TLS reverse proxy ahead of it before exposing it beyond a trusted network.
  • Rotate the per VM credential into your secret store and treat /root/pathling-credentials.txt as the sensitive file it is. Rotate with sudo htpasswd -B /etc/nginx/pathling.htpasswd pathling then sudo systemctl reload nginx.
  • Keep the firewall in place. The image ships ufw with default deny inbound and only 22 + 80 allowed; widen it only deliberately.
  • Leave unattended-upgrades enabled so the base system keeps receiving security updates.

Troubleshooting

The server does not respond. Check the three services and the container's own log:

sudo systemctl status pathling --no-pager
sudo docker logs pathling 2>&1 | tail -n 40

Pathling starts a Spark JVM, so allow it a minute or so after boot before GET /fhir/metadata returns 200.

No credentials file. It is written by pathling-firstboot.service on the first boot only. Check what happened:

sudo tail -n 30 /var/log/cloudimg-firstboot.log

A request returns 401. Every path except GET /fhir/metadata requires the per VM credential. Confirm you are sending user pathling with the password from /root/pathling-credentials.txt, and that you rotated nginx (sudo systemctl reload nginx) if you changed it.

Out of memory under load. The Spark heap is capped at 4 GiB to fit Standard_B2ms. For heavier analytics, move to a larger SKU and raise JAVA_TOOL_OPTIONS=-Xmx in /etc/pathling-appliance/pathling.env in proportion, then sudo systemctl restart pathling.

Further reading

Support

This image is published by cloudimg. For help with the image itself, contact support@cloudimg.co.uk.