Pathling FHIR Analytics Server on Ubuntu 24.04 on Azure User Guide
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/metadatapublic -
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
ufwfirewall (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:
- resolves the VM's public IP for the info file,
- generates a unique HTTP Basic Auth password for the
pathlinguser, - writes it into the nginx credential file
/etc/nginx/pathling.htpasswd, - writes
/root/pathling-credentials.txt(mode 600, owned by root), - 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>

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.

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.

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.

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.txtas the sensitive file it is. Rotate withsudo htpasswd -B /etc/nginx/pathling.htpasswd pathlingthensudo systemctl reload nginx. - Keep the firewall in place. The image ships
ufwwith default deny inbound and only 22 + 80 allowed; widen it only deliberately. - Leave
unattended-upgradesenabled 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
- Pathling documentation: https://pathling.csiro.au/docs
- Pathling source and releases: https://github.com/aehrc/pathling
- SQL on FHIR v2 specification: https://sql-on-fhir.org
- HL7 FHIR R4 specification: https://hl7.org/fhir/R4/
Support
This image is published by cloudimg. For help with the image itself, contact support@cloudimg.co.uk.