Deployment guide

Eclipse RDF4J

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.

This SecureAnyCloud image installs RDF4J 5.3.2 with both official web applications deployed into Apache Tomcat 9. RDF4J Server is the triplestore and its SPARQL endpoint. RDF4J Workbench is the browser-based 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, whether from a browser, curl or a SPARQL client, must authenticate.

What you get:

  • 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-based 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, and 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 SecureAnyCloud support

Prerequisites

You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.

Standard_B2ms (2 vCPU / 8 GiB RAM) is a reasonable starting size. Choose a larger size for bigger datasets.

Configure the network security group (NSG) to allow inbound 22/tcp from your management network and 80/tcp for the Workbench and the SPARQL endpoint. The image serves plain HTTP on port 80, so 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 SecureAnyCloud, and select Create.

On the Basics tab, 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).

Select Review + create, then Create.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:rdf4j-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name rdf4j \
  --image sac1647283583153:rdf4j-ubuntu-24-04:default:latest \
  --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 the credentials file:

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 does not yet show the generated password, wait for rdf4j-firstboot.service to finish:

sudo systemctl is-active rdf4j-firstboot.service

Step 4 - Verify the deployment

Confirm that 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, and the Workbench returns 401. This is the intended behavior.

Tomcat listens on loopback only. Confirm that nothing but nginx is exposed:

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

nginx listens publicly on port 80, and Tomcat on 8080 is bound to [::ffff:127.0.0.1], the IPv6-mapped form of 127.0.0.1 (loopback). RDF4J is therefore reachable from the network only 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. Enter username admin and the 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. This 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. Run the following to create 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

An HTTP 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 your data to the repository's statements endpoint. Turtle, N-Triples, RDF/XML and JSON-LD are all accepted:

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

Send a query over HTTP and request 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'

Send Accept: application/sparql-results+json to receive JSON instead. The same query on the Workbench Query page renders as a table.

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

Step 9 - Run a SPARQL update

Send INSERT DATA, DELETE WHERE and the rest of SPARQL 1.1 Update 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 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, and only the per-VM credential returns 200.

With the per-VM credential, the full round trip works: list repositories, count triples and run a SPARQL SELECT.

Step 11 - Change the password

The Basic-auth credential lives in an nginx htpasswd file. Run the following to rotate the password or add a second user:

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

Afterwards, update /root/rdf4j-credentials.txt 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, 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 of it before you expose the VM to an untrusted network. Use either an Azure Application Gateway or Front Door to terminate 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 sales@secureanycloud.com with your Azure subscription ID and the VM name.

Deploy on Azure

Launch Eclipse RDF4J 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

Deploy on Azure