eXist-db 6.4
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. XQuery 3.1, XPath and XSLT can therefore 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.
This image installs eXist-db 6.4.1 from the official upstream distribution, alongside OpenJDK 17. The download is pinned by both byte length and SHA-256. Upstream publishes no checksum file, so the digest is recorded in the build recipe.
The bundled application catalog is deployed at first boot, with no network access required. It includes Dashboard, the eXide XQuery editor, the package manager, monex and the documentation apps.
Security is the main difference from a stock install. A stock eXist-db install creates its admin account with an empty password and its guest account with the password guest. It also creates one account per bundled application package, and upstream sets each of those passwords 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 and sets a random per-VM administrator password. It rotates every other account to a value it discards and proves that each published default is rejected. The public listener stays shut until all of that has succeeded.
What you get:
- eXist-db 6.4.1 at
/opt/existdb, with the database at/var/lib/existdb - OpenJDK 17 JRE headless from Ubuntu 24.04 noble main
- The bundled application catalog: Dashboard, eXide, package manager, monex and documentation
existdb.servicerunning as the unprivilegedexistdbuser, bound to127.0.0.1:8080- An nginx reverse proxy on port 80 with an unauthenticated
/healthendpoint - 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 SecureAnyCloud support
Prerequisites
You need an active Azure subscription, an SSH key, and a 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 serves as 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.
For NSG inbound rules, 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. For production, terminate TLS upstream with your own certificate and domain. Azure Application Gateway, Front Door or a TLS-terminating reverse proxy all work.
Step 1-3: Deploy and connect over SSH
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 catalog. This takes about 45 seconds.
Port 80 deliberately stays closed until first boot has finished and every credential check has passed. A refused connection on port 80 right after deployment therefore means first boot is still running.
Service Status and Health
Confirm that both services are active and that the unauthenticated health endpoint answers. Also confirm 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)$' | sortBoth units report active. /health returns HTTP 200. nginx serves it directly, so it is a true liveness probe for the public listener and never depends on the database's auth state.
A request to / returns HTTP 302, which redirects 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. The IPv4-mapped form is still loopback, so the database is not reachable from the network.
Read the Per-VM Administrator Credentials
The administrator password is unique to this VM. First boot writes it to a root-only file:
sudo cat /root/existdb-credentials.txtThe file prints EXISTDB_URL, EXISTDB_ADMIN_USER (admin) and EXISTDB_ADMIN_PASSWORD (the per-VM password). Keep it safe. The password unlocks the Dashboard, the eXide editor, the package manager and the full REST API. No two SecureAnyCloud deployments share this password.
Every command below reads the password straight from that file into a shell variable, so you paste nothing:
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 -1The database reports version 6.4.1 and the JVM reports openjdk version "17.0.19".
The Authentication Wall
Run this check on a fresh deployment. A stock eXist-db install fails it. 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 that eXist-db creates for each bundled application package: eXide, monex and packageservice. Their upstream password is the account name. It also holds 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'; echoThe accounts present are SYSTEM, admin, eXide, guest, monex, nobody, packageservice.
Store a Document and Query It
Store an XML document with a REST PUT, then query it with XQuery. The PUT creates collections implicitly:
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'; echoThe PUT returns HTTP 201. The FLWOR expression returns Native XML storage | XQuery in practice, which reads the titles back in document order straight out of the stored XML.
The collection() count returns 2. It queries across every resource in the collection, not a single document.
Disk, Memory and the Deployed Catalogue
Confirm where the database lives, that the image runs with no swap, and which applications first boot deployed:
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'; echoThe 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 Dashboard
Open http://<vm-ip>/ in a browser. The redirect takes you to the eXist-db launcher. It reports the running version and lists the applications that first boot deployed: eXide (the XQuery IDE), eXist-db Documentation, Monex (monitoring) and XQuery Function Documentation.
Select Login at the top right and sign in as admin with the per-VM password from Step 5: you can browse the launcher without signing in, but saving and administration need a login.
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. 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. You can open, edit and save any stored resource in place.
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. This sign-in is the most direct proof that the per-VM credential works in a real browser session.
It lands on the monitoring dashboard, which shows active database brokers, uptime, running queries, waiting threads, and a live Java memory chart.
Use the memory panel 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. Monex therefore reports the maximum as roughly 989 MiB, with only a small fraction in use on an idle instance. Monex also exposes index and profiling views for tuning queries.
Step 11b: Look up XQuery functions
The XQuery Function Documentation app indexes every function module available in this instance. That includes the extension modules that eXist-db adds on top of the W3C standard library.
Type a name and select Search to get the module, the exact arity-qualified signature and its description. For example, searching passwd returns the securitymanager module and sm:passwd#2. This is the function that the image's first-boot script uses to set the per-VM administrator password.
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. eXist-db's own caches are sized to fit inside it (cacheSize="192M" and collectionCache="48M" in /opt/existdb/etc/conf.xml).
For large corpora, raise all three values together, 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.serviceDo not add a swap file to make a larger heap fit, because a swap file on the OS disk fails Azure image certification. Move to a larger VM size instead.
REST, RESTXQ and XML-RPC
The REST interface at /exist/rest/db/... stores and retrieves resources with PUT, GET and DELETE. It evaluates XQuery through the _query parameter, as used throughout this guide.
RESTXQ lets you publish your own HTTP endpoints. Annotate XQuery functions with %rest:GET and %rest:path, store them in the database, and eXist-db serves them under /exist/restxq/.
XML-RPC is available at /exist/xmlrpc for the Java and Python client libraries.
All three interfaces honor the same accounts and permissions, so the per-VM administrator password authenticates every one of them.
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 that the output has the right owner. Then copy the result off the VM, for example to Azure Files, Blob storage or your own backup target.
You can also configure a scheduled backup 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>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 | headIf port 80 refuses connections, check the first-boot journal above. nginx is gated on the marker /var/lib/sac/data/existdb-bootstrap-ready. First boot creates the marker only after the per-VM password is set and every default credential has been proven rejected. This is deliberate, so the listener never opens onto an unconfigured database.
If first boot failed, the journal names the failing step. nginx stays down until first boot 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/. This image disables the HTTPS connector and removes the distribution's bundled keystore, 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 upstream distribution ships an HTTPS connector. This image disables it and deletes its bundled keystore.p12, because that keystore is a publicly downloadable private key.
The image handles every credential that eXist-db creates before anything is reachable. It contains no database, so it ships with no accounts at all. First boot does the following:
- Generates a random per-VM
adminpassword into/root/existdb-credentials.txt(mode 0600, root-owned). - Rotates
guestand every package-owner account to random values that are immediately discarded. - Proves that the empty
adminpassword,guest/guest, each package account's own name, and the internalSYSTEMandnobodyaccounts are all rejected. - Writes the marker, which allows port 80 to open.
If any of those checks fail, first boot never creates the marker and the instance stays closed, rather than exposing an unconfigured database.
Port 80 serves plain HTTP. Before you expose the instance to the internet, terminate TLS upstream with your own certificate and domain, and restrict the NSG to known client CIDRs. Create per-person accounts in the user manager instead of sharing the admin password.
Support
SecureAnyCloud provides 24/7 support for this image. Email sales@secureanycloud.com.
The $0.04 per vCPU hour SecureAnyCloud 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 SecureAnyCloud charge is for the managed, patched, support-backed image.
Deploy on Azure
Launch eXist-db 6.4.1 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com