Eclipse RDF4J on Ubuntu 24.04 on Azure User Guide
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:8080only, 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
/healthendpoint 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.

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 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 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.

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:

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

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.

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

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.