Databases Azure

eXist-db 6.4 on Ubuntu 24.04 on Azure User Guide

| Product: eXist-db 6.4.1 on Ubuntu 24.04 LTS on Azure

Overview

eXist-db is an open source native XML database and application platform. It stores XML, JSON and binary resources in hierarchical collections with their structure intact, so XQuery 3.1, XPath and XSLT can address any element or attribute directly, and range, full text and n-gram indexes keep those lookups fast over large corpora. It is also an application runtime: XQuery and XSLT execute inside the database, RESTXQ turns annotated functions into a REST API, and a plain REST interface stores and retrieves documents over HTTP.

The cloudimg image installs eXist-db 6.4.1 from the official upstream distribution, pinned by both byte length and SHA-256 (upstream publishes no checksum file, so the digest is recorded in the build recipe), alongside OpenJDK 17. The bundled application catalogue — Dashboard, the eXide XQuery editor, the package manager, monex and the documentation apps — is deployed at first boot with no network access required.

Security is the headline difference in this image. A stock eXist-db install creates its admin account with an empty password and its guest account with the password guest, and eXist also creates one account per bundled application package whose password upstream sets equal to the account name. This image ships with no database at all, so none of those credentials exist in any state. On first boot the VM builds its own database, sets a random per-VM administrator password, rotates every other account to a value it discards, and proves each published default is rejected — and the public listener stays shut until all of that has succeeded.

What is included:

  • eXist-db 6.4.1 at /opt/existdb, database at /var/lib/existdb
  • OpenJDK 17 JRE headless from Ubuntu 24.04 noble main
  • The bundled application catalogue: Dashboard, eXide, package manager, monex, documentation
  • existdb.service running as the unprivileged existdb user, bound to 127.0.0.1:8080
  • An nginx reverse proxy on port 80 with an unauthenticated /health endpoint
  • A per-VM administrator password generated at first boot, in /root/existdb-credentials.txt (mode 0600)
  • Every upstream default credential rotated and proven rejected before port 80 opens
  • The JVM heap capped explicitly at 1 GiB and no swap anywhere in the image
  • 24/7 cloudimg support

Prerequisites

Active Azure subscription, SSH key, VNet and subnet. Standard_B2ms (2 vCPU, 8 GiB) is the recommended size: the JVM heap is capped at 1 GiB and the remaining memory is useful operating-system page cache for a disk-backed document store. The image also runs within 4 GiB (Standard_B2s) with no swap. For large corpora raise the VM size and the heap together, as shown in Step 12.

NSG inbound: allow 22/tcp from your management CIDR and 80/tcp from the client CIDRs that need the database. eXist-db's own listener is bound to loopback and is never reachable from the network; nginx on port 80 is the only public listener. Port 80 serves plain HTTP — terminate TLS upstream (Azure Application Gateway, Front Door, or a TLS-terminating reverse proxy) with your own certificate and domain before production use.

Step 1-3: Deploy + SSH (standard pattern)

Deploy the VM from the Azure Marketplace offer, then connect over SSH as azureuser:

ssh azureuser@<vm-ip>

First boot builds the database and deploys the bundled application catalogue. This takes about 45 seconds; port 80 deliberately stays closed until it has finished and every credential check has passed, so a connection refused on port 80 immediately after deployment simply means first boot is still running.

Step 4: Service Status and Health

Confirm both services are active, that the unauthenticated health endpoint answers, and that the database itself is bound to loopback only:

sudo systemctl is-active existdb.service nginx.service
curl -s -o /dev/null -w 'health:   HTTP %{http_code}\n' http://127.0.0.1/health
curl -s -o /dev/null -w 'redirect: HTTP %{http_code}\n' http://127.0.0.1/
ss -tlnH | awk '{print $4}' | grep -E ':(80|8080)$' | sort

Both units report active. /health returns HTTP 200 — this is served by nginx itself, so it is a true public-listener liveness probe that never depends on the database's auth state. A request to / returns HTTP 302, redirecting to eXist-db's /exist/ context path. The listener list shows nginx on 0.0.0.0:80 and [::]:80, and eXist-db on [::ffff:127.0.0.1]:8080 — that IPv4-mapped form is still loopback, so the database is not reachable from the network.

existdb.service and nginx.service both active, nginx /health returning HTTP 200, / returning HTTP 302 to the eXist context path, and the listening sockets showing nginx on 0.0.0.0:80 with eXist-db bound to loopback on 8080

Step 5: Read the Per-VM Administrator Credentials

The administrator password is unique to this VM and is written to a root-only file at first boot:

sudo cat /root/existdb-credentials.txt

This prints EXISTDB_URL, EXISTDB_ADMIN_USER (admin) and EXISTDB_ADMIN_PASSWORD (the per-VM password). Keep it safe: it unlocks the Dashboard, the eXide editor, the package manager and the full REST API. No two cloudimg deployments share this password.

Every command below reads the password straight out of that file into a shell variable, so nothing needs to be pasted:

PW=$(sudo grep '^EXISTDB_ADMIN_PASSWORD=' /root/existdb-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" -G http://127.0.0.1/exist/rest/db \
  --data-urlencode '_query=system:get-version()' --data-urlencode '_wrap=no'; echo
java -version 2>&1 | head -1

The database reports version 6.4.1 and the JVM reports openjdk version "17.0.19".

Step 6: The Authentication Wall

This is the check worth running on a fresh deployment, because it is exactly what a stock eXist-db install fails. Each probe asks the database to evaluate the trivial XQuery 1 and prints only the HTTP status:

PW=$(sudo grep '^EXISTDB_ADMIN_PASSWORD=' /root/existdb-credentials.txt | cut -d= -f2-)
Q() { curl -s -o /dev/null -w '%{http_code}' -u "$1:$2" -G http://127.0.0.1/exist/rest/db \
        --data-urlencode '_query=1' --data-urlencode '_wrap=no'; }
printf 'wrong password:        HTTP %s\n' "$(Q admin definitely-wrong)"
printf 'upstream empty admin:  HTTP %s\n' "$(Q admin '')"
printf 'upstream guest/guest:  HTTP %s\n' "$(Q guest guest)"
printf 'per-VM admin password: HTTP %s\n' "$(Q admin "$PW")"

A wrong password, the upstream empty admin password and the upstream guest/guest pair all return HTTP 401. Only the per-VM password returns HTTP 200. The same holds for the accounts eXist-db creates for each bundled application package — eXide, monex and packageservice, whose upstream password is the account name — and for the internal SYSTEM and nobody accounts. You can list every account and re-run the probe against any of them:

PW=$(sudo grep '^EXISTDB_ADMIN_PASSWORD=' /root/existdb-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" -G http://127.0.0.1/exist/rest/db \
  --data-urlencode '_query=string-join(sm:list-users(),", ")' --data-urlencode '_wrap=no'; echo

The accounts present are SYSTEM, admin, eXide, guest, monex, nobody, packageservice.

The authentication wall: a wrong password, the upstream empty admin password and the upstream guest/guest pair all returning HTTP 401 while the per-VM password returns HTTP 200, followed by the full account list

Step 7: Store a Document and Query It

This is what the database is for. Store an XML document with a REST PUT, then query it with XQuery. Collections are created implicitly by the PUT:

PW=$(sudo grep '^EXISTDB_ADMIN_PASSWORD=' /root/existdb-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'PUT catalog.xml: HTTP %{http_code}\n' -u "admin:$PW" \
  -X PUT -H 'Content-Type: application/xml' \
  --data-binary '<catalog><book id="b1"><title>Native XML storage</title></book><book id="b2"><title>XQuery in practice</title></book></catalog>' \
  http://127.0.0.1/exist/rest/db/demo/catalog.xml
curl -s -u "admin:$PW" -G http://127.0.0.1/exist/rest/db \
  --data-urlencode "_query=string-join(for \$b in doc('/db/demo/catalog.xml')//book order by \$b/@id return string(\$b/title),' | ')" \
  --data-urlencode '_wrap=no'; echo
curl -s -u "admin:$PW" -G http://127.0.0.1/exist/rest/db \
  --data-urlencode "_query=count(collection('/db/demo')//book)" --data-urlencode '_wrap=no'; echo

The PUT returns HTTP 201. The FLWOR expression returns Native XML storage | XQuery in practice, reading the titles back in document order straight out of the stored XML. The collection() count returns 2, querying across every resource in the collection rather than a single document.

A REST PUT storing catalog.xml returning HTTP 201, an XQuery FLWOR expression returning both stored book titles, and a collection() count returning 2

Step 8: Disk, Memory and the Deployed Catalogue

Confirm where the database lives, that the image runs with no swap, and which applications were deployed at first boot:

PW=$(sudo grep '^EXISTDB_ADMIN_PASSWORD=' /root/existdb-credentials.txt | cut -d= -f2-)
df -h /var/lib/existdb | awk 'NR==1 || /\/$/'
free -m | head -3
curl -s -u "admin:$PW" -G http://127.0.0.1/exist/rest/db \
  --data-urlencode '_query=string-join(xmldb:get-child-collections("/db/apps"),", ")' --data-urlencode '_wrap=no'; echo

The database directory is /var/lib/existdb. free -m shows the Swap: row at zero across the board — Azure manages swap on the ephemeral resource disk, and a swap file baked into an OS disk fails image certification, so this image ships without one. The deployed applications are packageservice, dashboard, eXide, doc, fundocs, monex.

The eXist-db data directory on the OS disk, a free -m showing zero swap with ample memory available, and the list of applications deployed at first boot

Step 9: The eXist-db Dashboard

Open http://<vm-ip>/ in a browser. The redirect takes you to the eXist-db launcher, which reports the running version and lists the applications deployed at first boot: eXide (the XQuery IDE), eXist-db Documentation, Monex (monitoring) and XQuery Function Documentation. Use Login in the top-right to sign in as admin with the per-VM password from Step 5 — the launcher itself is browsable without signing in, but saving and administration are not.

The eXist-db launcher reporting Version 6.4.1 with tiles for eXide - XQuery IDE, eXist-db Documentation, Monex and XQuery Function Documentation

Step 10: Run an XQuery in eXide

Open eXide from the Dashboard. eXide is eXist-db's in-browser XQuery editor: it edits resources stored in the database, runs queries against live data and shows the results underneath. Paste a query and run it with Eval (or Ctrl-Enter):

xquery version "3.1";
for $b in doc('/db/demo/catalog.xml')//book
order by $b/@id
return <result id="{$b/@id}">{string($b/title)}</result>

The results panel renders the constructed <result> elements for both stored books, and the status toast reports Query returned 2 item(s) with the evaluation time — the query ran inside the database against the document stored in Step 7, with no export step. eXide's directory and outline panes on the left browse the database hierarchy, so you can open, edit and save any stored resource in place.

The eXide XQuery editor with an XQuery 3.1 FLWOR expression in the editor pane, the results panel rendering the two constructed result elements for the stored books, and a toast reporting the query returned 2 items

Step 11: Monitor the Instance with Monex

Open Monex from the launcher. It asks you to sign in as a dba user: enter admin and the per-VM password from Step 5. That sign-in is the most direct proof the per-VM credential works in a real browser session, and it lands on the monitoring dashboard: active database brokers, uptime, running queries, waiting threads, and a live Java memory chart.

The memory panel is a good place to confirm the heap. This image caps the JVM heap explicitly at 1 GiB rather than letting the JVM take a quarter of system memory, so Monex reports the maximum as roughly 989 MiB, with only a small fraction of it in use on an idle instance. Monex also exposes index and profiling views for tuning queries.

The Monex monitoring dashboard signed in as admin, showing active database brokers, uptime, running queries and a live Java memory chart reporting 68 of 989 MiB used

Step 11b: Look Up XQuery Functions

The XQuery Function Documentation app indexes every function module available in this instance, including the extension modules eXist-db adds on top of the W3C standard library. Type a name and press Search to get the module, the exact arity-qualified signature and its description. Searching passwd, for example, returns the securitymanager module and sm:passwd#2 — the function this image's first-boot script uses to set the per-VM administrator password.

The XQuery Function Documentation app returning search results for passwd, showing the securitymanager module and the sm:passwd#2 signature with its description

Step 12: Tune the JVM Heap

The heap is capped explicitly in /etc/existdb.env (-Xms256m -Xmx1024m) rather than left to the JVM's default of a quarter of system memory, and eXist-db's own caches are sized to fit inside it (cacheSize="192M", collectionCache="48M" in /opt/existdb/etc/conf.xml). Raise all three together for large corpora, then restart. eXist-db's own guidance is to keep cacheSize at roughly a third of the maximum heap:

sudo sed -i 's/-Xmx1024m/-Xmx3072m/' /etc/existdb.env
sudo sed -i 's/cacheSize="192M"/cacheSize="768M"/' /opt/existdb/etc/conf.xml
sudo systemctl restart existdb.service

Do not add a swap file to make a larger heap fit — a swap file on the OS disk fails Azure image certification. Move to a larger VM size instead.

Step 13: REST, RESTXQ and XML-RPC

The REST interface at /exist/rest/db/... stores and retrieves resources with PUT, GET and DELETE, and evaluates XQuery via the _query parameter, as used throughout this guide. RESTXQ lets you publish your own HTTP endpoints by annotating XQuery functions with %rest:GET and %rest:path, stored in the database and served under /exist/restxq/. XML-RPC is available at /exist/xmlrpc for the Java and Python client libraries. All three honour the same accounts and permissions, so the per-VM administrator password authenticates every one of them.

Step 14: Backups

eXist-db ships a consistency-checking backup tool that writes a full or incremental backup of the database to a directory. Run it as the existdb user so the output is owned correctly, and copy the result off the VM (Azure Files, Blob storage, or your own backup target). A scheduled backup can also be configured from the Dashboard's backup application.

sudo -u existdb EXIST_HOME=/opt/existdb /opt/existdb/bin/backup.sh \
  -u admin -p '<new-password>' -b /db -d <backup-dir>

Step 15: Logs and Troubleshooting

eXist-db logs to /opt/existdb/logs and the service journal:

sudo journalctl -u existdb.service --no-pager | tail -20
sudo journalctl -u exist-db-firstboot.service --no-pager | tail -20
sudo ls -1 /opt/existdb/logs | head

If port 80 refuses connections, check the first-boot journal above: nginx is gated on the marker /var/lib/cloudimg/existdb-bootstrap-ready, which first boot creates only after the per-VM password is set and every default credential has been proven rejected. That is deliberate — the listener never opens onto an unconfigured database. If first boot failed, the journal names the failing step; nginx stays down until it succeeds.

existdb.service reads its environment from /etc/existdb.env and its main configuration is /opt/existdb/etc/conf.xml. The Jetty connector configuration is under /opt/existdb/etc/jetty/; the HTTPS connector is disabled in this image and the distribution's bundled keystore removed, because TLS terminates at nginx or upstream.

Security

eXist-db's own listener is bound to 127.0.0.1:8080 and is never reachable from the network; nginx on port 80 is the only public listener, and it cannot start until first boot has written its bootstrap-ready marker. The HTTPS connector shipped in the upstream distribution is disabled and its bundled keystore.p12 deleted, because that keystore is a publicly downloadable private key.

Every credential eXist-db creates is dealt with before anything is reachable. The image contains no database, so it ships with no accounts at all. First boot generates a random per-VM admin password into /root/existdb-credentials.txt (mode 0600, root-owned), rotates guest and every package-owner account to random values that are immediately discarded, and then proves that the empty admin password, guest/guest, each package account's own name, and the internal SYSTEM and nobody accounts are all rejected. Only then is the marker written and port 80 allowed to open. If any of those checks fail, the marker is never created and the instance stays closed rather than exposing an unconfigured database.

Port 80 serves plain HTTP. Terminate TLS upstream with your own certificate and domain, and restrict the NSG to known client CIDRs, before exposing the instance to the internet. Create per-person accounts in the user manager instead of sharing the admin password.

Support

cloudimg provides 24/7 support for this image. Email support@cloudimg.co.uk. The $0.04 per vCPU hour cloudimg charge covers packaging, security patching, image maintenance, and expert support. eXist-db is licensed under the GNU Lesser General Public License v2.1 and is free; the cloudimg charge is for the managed, patched, support-backed image.