Er
Databases Azure

Eclipse RDF4J on Ubuntu 24.04 on Azure User Guide

| Product: Eclipse RDF4J on Ubuntu 24.04 LTS on Azure

Overview

Eclipse RDF4J is a Java framework and server for RDF and linked data. It stores RDF triples and answers SPARQL 1.1 queries and updates over HTTP. The cloudimg image installs RDF4J 5.3.2 with both official web applications deployed into Apache Tomcat 9: RDF4J Server, the triplestore and its SPARQL endpoint, and RDF4J Workbench, the browser admin UI for creating repositories, loading data and running queries.

Read this before you expose the VM. Upstream RDF4J ships with no authentication of any kind — its web.xml carries the security constraint commented out — and its SPARQL endpoint accepts UPDATE and DELETE. On a stock deployment, anything that can reach the port can create or wipe every repository. This image closes that gap: Tomcat is bound to 127.0.0.1 only, and nginx is the sole public listener, enforcing HTTP Basic authentication with a password generated uniquely on the first boot of every VM. Every request — browser, curl, or a SPARQL client — must authenticate.

What is included:

  • Eclipse RDF4J 5.3.2 (BSD-3-Clause / Eclipse Distribution License v1.0) on OpenJDK 17
  • Apache Tomcat 9.0.120, bound to 127.0.0.1:8080 only, with its shutdown port disabled
  • RDF4J Server at /rdf4j-server — the triplestore plus the SPARQL 1.1 query and update endpoint
  • RDF4J Workbench at /rdf4j-workbench — the browser admin UI
  • nginx on port 80 as the only public listener, enforcing HTTP Basic authentication for every path
  • A per-VM password generated on first boot and recorded in a root-only file
  • File-backed native-store repositories under /var/lib/rdf4j
  • An empty store on first boot — you create the first repository, nothing is pre-seeded
  • No Tomcat accounts and no Tomcat manager, host-manager, examples or docs webapps
  • An unauthenticated /health endpoint for Azure Load Balancer health probes
  • 24/7 cloudimg support

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2ms (2 vCPU / 8 GiB RAM) is a reasonable starting point; size up for larger datasets. NSG inbound: allow 22/tcp from your management network and 80/tcp for the Workbench and the SPARQL endpoint. The image serves plain HTTP on port 80; for production, terminate TLS in front of it with your own domain.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Eclipse RDF4J by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create -> Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name rdf4j \
  --image <marketplace-image-urn> \
  --size Standard_B2ms \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard \
  --os-disk-delete-option Delete --nic-delete-option Delete

Step 3 - Retrieve the per-VM password

Every VM generates its own password on first boot. Nothing is shared between deployments and there is no default login. SSH in and read it:

sudo cat /root/rdf4j-credentials.txt

The file is mode 0600 and owned by root. It records the Workbench and server URLs, the username admin, and RDF4J_ADMIN_PASSWORD.

The per-VM RDF4J credentials file, the deployed RDF4J version and the capped JVM heap settings

First boot normally completes within a minute of the VM reaching Running. If the file still shows the placeholder text, wait for rdf4j-firstboot.service to finish:

sudo systemctl is-active rdf4j-firstboot.service

Step 4 - Verify the deployment

Confirm the services are up, the health probe answers, and RDF4J is reachable only through the authenticated proxy:

sudo systemctl is-active tomcat.service nginx.service rdf4j-firstboot.service
curl -s -o /dev/null -w 'health (no auth):    HTTP %{http_code}\n' http://127.0.0.1/health
curl -s -o /dev/null -w 'workbench (no auth): HTTP %{http_code}\n' http://127.0.0.1/rdf4j-workbench/

The health probe returns 200 without credentials; the Workbench returns 401. That is the intended behaviour.

Tomcat, nginx and the first-boot unit all active, the health probe returning 200 and the Workbench returning 401 without credentials

Tomcat listens on loopback only. You can confirm nothing but nginx is exposed:

ss -tlnH | awk '{print $4}' | grep -E ':(80|8080)$' | sort -u

You will see nginx listening publicly on port 80, and Tomcat on 8080 bound to [::ffff:127.0.0.1] — that is the IPv6-mapped form of 127.0.0.1, i.e. loopback. RDF4J is therefore not reachable from the network at all except through the authenticated nginx listener on port 80.

Step 5 - Sign in to the Workbench

Open http://<your-vm-public-ip>/ in a browser. You are redirected to the Workbench and prompted for HTTP Basic credentials: username admin, password from Step 3. The store is empty on a new VM, so the repository list starts with no entries.

The RDF4J Workbench List of Repositories page

The header shows RDF4J Server: http://127.0.0.1:8080/rdf4j-server. That is correct and deliberate: the Workbench reaches the server directly over loopback, behind the nginx authentication layer, rather than looping back through the public port.

Step 6 - Create your first repository

In the Workbench choose New repository, pick a type, and give it an ID and title. Native Store keeps data on disk and is the right choice for most deployments; the Type list opens on Memory Store, which is not persisted across a restart unless you enable persistence.

The RDF4J Workbench New Repository form, with the Type list, ID and Title fields

You can do the same over the REST API. This creates a native-store repository called example:

PW=$(sudo grep '^RDF4J_ADMIN_PASSWORD=' /root/rdf4j-credentials.txt | cut -d= -f2-)
cat > /tmp/example-repo.ttl <<'TTL'
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix config: <tag:rdf4j.org,2023:config/>.

[] a config:Repository ;
   config:rep.id "example" ;
   rdfs:label "Example repository" ;
   config:rep.impl [
      config:rep.type "openrdf:SailRepository" ;
      config:sail.impl [
         config:sail.type "openrdf:NativeStore" ;
         config:native.tripleIndexes "spoc,posc"
      ]
   ].
TTL
curl -s -o /dev/null -w 'create repository: HTTP %{http_code}\n' -u "admin:$PW" \
  -X PUT -H 'Content-Type: text/turtle' --data-binary @/tmp/example-repo.ttl \
  http://127.0.0.1/rdf4j-server/repositories/example

204 means the repository was created. List the repositories to confirm:

PW=$(sudo grep '^RDF4J_ADMIN_PASSWORD=' /root/rdf4j-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" -H 'Accept: text/csv' http://127.0.0.1/rdf4j-server/repositories

Step 7 - Load RDF data

Post Turtle (or N-Triples, RDF/XML, JSON-LD) to the repository's statements endpoint:

PW=$(sudo grep '^RDF4J_ADMIN_PASSWORD=' /root/rdf4j-credentials.txt | cut -d= -f2-)
cat > /tmp/example-data.ttl <<'TTL'
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix ex: <http://example.org/library/> .

ex:weaving      dc:title "Weaving the Web" ; dc:creator "Tim Berners-Lee" ; dc:date "1999" .
ex:primer       dc:title "A Semantic Web Primer" ; dc:creator "Grigoris Antoniou" ; dc:date "2004" .
ex:linkeddata   dc:title "Linked Data" ; dc:creator "David Wood" ; dc:date "2014" .
TTL
curl -s -o /dev/null -w 'load triples: HTTP %{http_code}\n' -u "admin:$PW" \
  -X POST -H 'Content-Type: text/turtle' --data-binary @/tmp/example-data.ttl \
  http://127.0.0.1/rdf4j-server/repositories/example/statements
printf 'triples now stored: '
curl -s -u "admin:$PW" http://127.0.0.1/rdf4j-server/repositories/example/size
echo

Step 8 - Run a SPARQL query

Query over HTTP, asking for CSV results:

PW=$(sudo grep '^RDF4J_ADMIN_PASSWORD=' /root/rdf4j-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" -H 'Accept: text/csv' -G \
  http://127.0.0.1/rdf4j-server/repositories/example \
  --data-urlencode 'query=PREFIX dc: <http://purl.org/dc/elements/1.1/>
    SELECT ?title ?creator ?year
    WHERE { ?b dc:title ?title ; dc:creator ?creator ; dc:date ?year }
    ORDER BY ?year'

Accept: application/sparql-results+json returns JSON instead. The same query in the Workbench's Query page renders as a table:

An executed SPARQL SELECT in the RDF4J Workbench, showing a six row Title, Creator and Year result table

The Workbench's Summary page reports the repository ID, its location and the number of stored statements:

The RDF4J Workbench Summary page showing the repository ID, location and statement count

Step 9 - Run a SPARQL update

INSERT DATA, DELETE WHERE and the rest of SPARQL 1.1 Update go to the same statements endpoint as an update parameter:

PW=$(sudo grep '^RDF4J_ADMIN_PASSWORD=' /root/rdf4j-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'insert: HTTP %{http_code}\n' -u "admin:$PW" \
  -X POST --data-urlencode \
  'update=PREFIX dc: <http://purl.org/dc/elements/1.1/>
   INSERT DATA { <http://example.org/library/rdf4j> dc:title "Learning SPARQL" ; dc:creator "Bob DuCharme" ; dc:date "2013" }' \
  http://127.0.0.1/rdf4j-server/repositories/example/statements
printf 'triples after insert: '
curl -s -u "admin:$PW" http://127.0.0.1/rdf4j-server/repositories/example/size
echo

Step 10 - Confirm the authentication layer

This is the most important check on the whole image, because upstream RDF4J has no access control of its own. Every destructive operation must be refused without the per-VM credential:

PW=$(sudo grep '^RDF4J_ADMIN_PASSWORD=' /root/rdf4j-credentials.txt | cut -d= -f2-)
C() { curl -s -o /dev/null -w '%{http_code}' -m 15 "$@" 2>/dev/null; }
S=http://127.0.0.1/rdf4j-server
printf 'anon   GET  repository list:        HTTP %s\n' "$(C $S/repositories)"
printf 'anon   POST SPARQL UPDATE DROP ALL: HTTP %s\n' "$(C -X POST --data-urlencode 'update=DROP ALL' $S/repositories/example/statements)"
printf 'anon   DELETE every triple:        HTTP %s\n' "$(C -X DELETE $S/repositories/example/statements)"
printf 'anon   DELETE the repository:      HTTP %s\n' "$(C -X DELETE $S/repositories/example)"
printf 'wrong  password:                   HTTP %s\n' "$(C -u admin:definitely-wrong $S/repositories)"
printf 'per-VM credential:                 HTTP %s\n' "$(C -u "admin:$PW" $S/repositories)"

Every anonymous and wrong-password attempt returns 401; only the per-VM credential returns 200.

Five anonymous and wrong password requests returning HTTP 401 and the per-VM credential returning HTTP 200

The full round trip — list repositories, count triples, run a SPARQL SELECT — with the per-VM credential:

Listing repositories, the triple count and a SPARQL SELECT returning a six row result set

Step 11 - Change the password

The Basic-auth credential lives in an nginx htpasswd file. To rotate it, or to add a second user:

sudo htpasswd -b /etc/nginx/.rdf4j_htpasswd admin '<new-password>'
sudo systemctl reload nginx

Update /root/rdf4j-credentials.txt afterwards so the recorded password stays accurate.

Maintenance

Service management. Tomcat hosts both web applications; nginx is the public listener.

sudo systemctl status tomcat.service --no-pager | head -12

Logs. Tomcat writes to /opt/tomcat/logs, and the units log to the journal:

sudo journalctl -u tomcat.service --no-pager | tail -20

Where the data lives. Repositories are stored under /var/lib/rdf4j/server/repositories, one directory per repository. Back up that path, or use the Workbench Export page, or GET the statements endpoint to dump a repository as RDF.

PW=$(sudo grep '^RDF4J_ADMIN_PASSWORD=' /root/rdf4j-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" -H 'Accept: text/turtle' \
  http://127.0.0.1/rdf4j-server/repositories/example/statements | head -12

JVM heap. The image caps Tomcat's heap explicitly at 1 GiB, which suits the default Standard_B2ms. For larger datasets, raise -Xmx in CATALINA_OPTS and restart Tomcat:

sudo systemctl edit tomcat.service
# add, adjusting to taste and leaving headroom for the OS:
#   [Service]
#   Environment="CATALINA_OPTS=-Xms512m -Xmx<your-heap-size> -XX:MaxMetaspaceSize=256m \
#     -Dorg.eclipse.rdf4j.appdata.basedir=/var/lib/rdf4j -Djava.awt.headless=true"
sudo systemctl daemon-reload && sudo systemctl restart tomcat.service

TLS. The image serves plain HTTP. Put TLS in front before exposing the VM to an untrusted network — either an Azure Application Gateway or Front Door terminating TLS, or certbot on the VM itself with your own domain:

sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain>

Updates. Unattended security upgrades are enabled. Apply OS updates and reboot on your own schedule:

sudo apt-get update && sudo apt-get -y dist-upgrade

Support

24/7 support is included with this image. Contact support@cloudimg.co.uk with your Azure subscription ID and the VM name.