E2
Applications Azure

ERDDAP on Ubuntu 24.04 on Azure User Guide

| Product: ERDDAP 2.30.0 on Ubuntu 24.04 LTS on Azure

Overview

ERDDAP is a scientific data server from NOAA's NMFS SWFSC Environmental Research Division. It sits in front of heterogeneous gridded and tabular datasets and gives all of them one consistent web interface and REST API, so a user or a script can request any subset of any dataset and get it back as CSV, JSON, netCDF, an HTML table, an image, a graph or a map. It is the de facto standard data server in oceanography, and it is what NOAA, IOOS, EMODnet, CSIRO and IFREMER use to publish their data.

This image runs ERDDAP 2.30.0 from the official upstream release on Apache Tomcat 10.1 under OpenJDK 25, both taken from Ubuntu's own package archive so they keep receiving security updates on your VM. Upstream's install instructions ask administrators to download and unpack a vendor JRE tarball; this image deliberately does not, because a hand-unpacked runtime is never patched again. Tomcat is bound to the loopback connector and fronted by nginx on port 80.

The catalogue works the moment the VM boots, with no external service involved. Upstream's example configuration is a catalogue of datasets that live on other people's servers, so a stock install shows an empty or broken list whenever those hosts are unreachable. This image instead ships three datasets whose data is already on the VM: the ETOPO1 global relief grid that is bundled inside ERDDAP itself, in both longitude conventions, and a small synthetic sample time series so the tabular half of the server is demonstrated too.

A unique administrative password and a unique dataset flag key are generated on the first boot of every VM. ERDDAP has no user database and no login of its own by design: a data server's catalogue and data endpoints are meant to be public. What this image fences off is the administrative surface, so the pages that expose internal state sit behind a per VM password. Backed by 24/7 cloudimg support.

What is included:

  • ERDDAP 2.30.0 on Apache Tomcat 10.1 under OpenJDK 25, both from Ubuntu's archive so security updates keep flowing
  • A working catalogue on first boot with three locally served datasets and no dependency on any third party server
  • The ETOPO1 one arc-minute global relief grid (NOAA NCEI, public domain), bundled in the image, ready to subset and map
  • Tomcat bound to loopback only and fronted by nginx on port 80, with unauthenticated /healthz and /readyz probes
  • The administrative pages — status, Prometheus metrics, dataset reload and subscriptions — behind a per VM password
  • A per VM dataset flag key replacing the placeholder value published in ERDDAP's own distribution
  • ERDDAP's subscription system, remote dataset subscription, data provider form and diagnostic phone-home all switched off by default
  • Configuration, cache and served data on a dedicated 32 GiB Azure data disk at /var/lib/erddap
  • A JVM heap sized from the VM's own memory on every first boot, so one image is correct on any VM size
  • tomcat10.service and nginx.service as systemd units, enabled and active
  • 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 the recommended size. ERDDAP is a single JVM whose heap this image sets to half of physical memory, following upstream's own guidance; upstream calls a 2 GB heap "okay" and 4+ GB "really good", which is what 8 GiB of RAM gives you. The image runs correctly on Standard_B2s (4 GiB) — that is what it is smoke tested on — but a server holding a real catalogue and serving concurrent subset requests deserves the extra headroom. NSG inbound: allow 22/tcp from your management network and 80/tcp (and 443/tcp if you add TLS). ERDDAP is served over plain HTTP by default, so for production put your own domain and a trusted certificate in front of it (see Maintenance).

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for ERDDAP 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). Review the dedicated data disk on the Disks tab, then Review + create then Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name erddap \
  --image <marketplace-image-urn> \
  --size Standard_B2ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

After the VM is created, open port 80 so you can reach the web interface:

az vm open-port --resource-group <your-rg> --name erddap --port 80 --priority 900

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

The first boot resolves the VM's public address, writes it into ERDDAP's configuration, generates the per VM secrets and starts the server. It normally completes within a minute of the VM becoming reachable.

Step 4 - Confirm the services are running

ERDDAP runs inside Tomcat behind nginx. Confirm both units are active, and confirm the first boot service completed:

systemctl is-active tomcat10.service nginx.service
systemctl is-active erddap-firstboot.service
test -f /var/lib/cloudimg/erddap-firstboot.done && echo "first boot completed"

The tomcat10, nginx and erddap-firstboot units all reporting active on a freshly booted VM, with the first boot sentinel present

Tomcat is bound to the loopback connector only, so ERDDAP is reachable exclusively through the nginx front end. Confirm that from the kernel's socket table rather than from a configuration file:

ss -ltnH | awk '{print $4}' | sed 's/.*://' | sort -u | tr '\n' ' '; echo
if ss -ltnH | awk '{print $4}' | grep -qE '^(0\.0\.0\.0|\[::\]|\*):8080$'; then
  echo "UNEXPECTED: Tomcat is bound to a wildcard address"; exit 1
fi
echo "Tomcat is not exposed directly - nginx on port 80 is the only front door"

Step 5 - Confirm ERDDAP is answering

nginx serves an unauthenticated liveness probe, and a readiness probe that is proxied straight through to ERDDAP so it only succeeds once the server is actually serving its catalogue:

code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost/healthz)
echo "GET /healthz -> HTTP $code"
[ "$code" = "200" ] || { echo "FAILED: the appliance is not answering"; exit 1; }

code=$(curl -s -o /dev/null -w '%{http_code}' -m 60 http://localhost/readyz)
echo "GET /readyz  -> HTTP $code"
[ "$code" = "200" ] || { echo "FAILED: ERDDAP is not serving yet"; exit 1; }

Step 6 - Retrieve your administrative password

ERDDAP itself has no login — its catalogue and data endpoints are public, which is the point of a data server. The administrative pages are fenced off instead, using a password generated for this VM and written to a root only file at /root/erddap-credentials.txt (mode 0600):

sudo cat /root/erddap-credentials.txt

The per VM credentials file listing the ERDDAP URL, the administrative user name, the resolved public IP source and the JVM heap chosen for this VM size

Confirm the administrative surface really is closed without that password, and really does open with it:

code=$(curl -s -o /dev/null -w '%{http_code}' -m 30 http://localhost/erddap/status.html)
echo "anonymous  -> HTTP $code"
[ "$code" = "401" ] || { echo "UNEXPECTED: the administrative page was not protected"; exit 1; }

code=$(curl -s -o /dev/null -w '%{http_code}' -m 30 \
  -u 'erddapadmin:<ERDDAP_ADMIN_PASSWORD>' http://localhost/erddap/status.html)
echo "per-VM key -> HTTP $code"
[ "$code" = "200" ] || { echo "FAILED: the per VM password did not authenticate"; exit 1; }

Step 7 - Browse the catalogue over the REST API

Everything ERDDAP offers in its web interface is also available as a REST call. List the datasets this server is holding:

curl -s 'http://localhost/erddap/info/index.json?page=1&itemsPerPage=1000' \
  | python3 -c "
import sys, json
t = json.load(sys.stdin)['table']
i = t['columnNames'].index('Dataset ID')
j = t['columnNames'].index('Title')
rows = t['rows']
print('%-22s %s' % ('DATASET ID', 'TITLE'))
for r in rows:
    print('%-22s %s' % (r[i], r[j][:52]))
ids = {r[i] for r in rows}
assert {'etopo180', 'etopo360', 'cloudimgDemoStation'} <= ids, 'expected datasets are missing'
print()
print('%d datasets loaded' % len(rows))
"

The dataset catalogue returned as JSON, listing the ETOPO1 global relief grid in both longitude conventions and the cloudimg sample station time series

Step 8 - Request real data

Ask the tabular dataset for three of its variables as CSV. ERDDAP returns a header row, a units row, and the data:

curl -s 'http://localhost/erddap/tabledap/cloudimgDemoStation.csv?time,station,sea_water_temperature,air_temperature' | head -14

Real measurements returned as CSV, with the variable names, their units and hourly rows of sea water and air temperature

The same request in JSON, checked properly — an ERDDAP error is returned with HTTP 200, so parse the payload rather than trusting the status code:

curl -s -m 120 'http://localhost/erddap/tabledap/cloudimgDemoStation.json?time,sea_water_temperature' \
  | python3 -c "
import sys, json
t = json.load(sys.stdin)['table']
rows = t['rows']
k = t['columnNames'].index('sea_water_temperature')
vals = [r[k] for r in rows]
assert all(isinstance(v, (int, float)) for v in vals), 'non-numeric values returned'
assert min(vals) != max(vals), 'the values do not vary - this is not real data'
print('%d rows returned, sea_water_temperature ranges %.3f to %.3f degree_C'
      % (len(rows), min(vals), max(vals)))
"

Change the extension to get the same subset in another format. .nc gives you netCDF, .htmlTable gives you a browser table, .json, .mat, .parquet, .geoJson and about thirty others are available from the same URL.

for ext in csv json nc htmlTable das; do
  ct=$(curl -s -o /dev/null -w '%{content_type}' -m 120 \
       "http://localhost/erddap/tabledap/cloudimgDemoStation.$ext?time,sea_water_temperature")
  sz=$(curl -s -o /dev/null -w '%{size_download}' -m 120 \
       "http://localhost/erddap/tabledap/cloudimgDemoStation.$ext?time,sea_water_temperature")
  printf '%-10s %-34s %s bytes\n' ".$ext" "$ct" "$sz"
  [ "${sz:-0}" -gt 500 ] || { echo "FAILED: .$ext returned almost nothing"; exit 1; }
done

Step 9 - Subset the gridded dataset

etopo180 is the ETOPO1 one arc-minute global relief grid, bundled inside the image. Request a small area of it and confirm real elevations come back:

curl -s -m 180 'http://localhost/erddap/griddap/etopo180.json?altitude%5B(36.0):1:(36.2)%5D%5B(-122.0):1:(-121.8)%5D' \
  | python3 -c "
import sys, json
t = json.load(sys.stdin)['table']
k = t['columnNames'].index('altitude')
vals = [r[k] for r in t['rows']]
assert vals, 'no grid values returned'
assert all(isinstance(v, (int, float)) for v in vals), 'non-numeric elevations'
print('%d grid cells returned, altitude %d m to %d m' % (len(vals), min(vals), max(vals)))
"

Step 10 - Draw a map

ERDDAP renders images server side. Ask it for a global topography and bathymetry map as a PNG and confirm what comes back really is a drawn image — an ERDDAP failure is served as an HTML page from this same URL, so check the bytes:

curl -s -m 300 -o /tmp/etopo.png 'http://localhost/erddap/griddap/etopo180.png?altitude%5B(-89.0):1:(89.0)%5D%5B(-179.0):1:(179.0)%5D&.draw=surface&.vars=longitude%7Clatitude%7Caltitude&.colorBar=Topography%7C%7C%7C%7C%7C&.land=under&.size=800%7C420'
python3 -c "
import struct
b = open('/tmp/etopo.png','rb').read()
assert b[:8] == b'\x89PNG\r\n\x1a\n', 'the response is not a PNG'
w, h = struct.unpack('>II', b[16:24])
assert w > 200 and h > 150, 'the image is too small to be a real map'
print('rendered a %dx%d PNG map, %d bytes' % (w, h, len(b)))
"

Step 11 - Confirm your data lives on the dedicated disk

ERDDAP's configuration, its cache and the data it serves all sit on the dedicated Azure data disk, so they are decoupled from the OS disk and the disk can be resized or snapshotted independently:

df -h /var/lib/erddap | tail -2
ls -1 /var/lib/erddap
echo "--- your configuration ---"
ls -1 /var/lib/erddap/content/erddap

Step 12 - Open ERDDAP in your browser

Browse to http://<vm-public-ip>/. You land on ERDDAP's home page, the front door to the whole catalogue:

The ERDDAP home page served from a freshly launched cloudimg VM, offering search, the dataset lists and the conversion tools

Step 13 - Browse the datasets

Choose List of All Datasets. Every dataset is listed with links to its gridded or tabular data access form, its Make A Graph page, its metadata and its source files:

The list of all datasets showing the ETOPO1 global relief grid in both longitude conventions and the cloudimg sample station time series, each with data, graph and metadata links

Step 14 - Use the Data Access Form

Click data next to a dataset to open its Data Access Form. This is where you choose variables, set the range of each dimension, pick an output file format and get a URL you can paste into a script:

The griddap Data Access Form for the ETOPO1 dataset, with the altitude variable, the latitude and longitude ranges and the file type selector

Step 15 - Draw maps and graphs in the browser

Click graph to open Make A Graph. The server renders the image and the form lets you change the graph type, the axes, the colour bar and the land mask, then redraw:

A global ETOPO1 bathymetry and topography map drawn by the server, with the topography colour bar, alongside the Make A Graph controls for graph type, axes and land mask

The same tool plots tabular data. Here the sample station's sea water temperature is plotted against time:

A sea water temperature time series plotted against time from the tabular sample dataset, with the Make A Graph constraint and marker controls beside it

Choosing .htmlTable as the file type instead gives you the data itself in the browser, with the units under each variable name:

A tabular data response rendered as an HTML table, showing the time, station, latitude, longitude and both temperature variables with their units and real values

Step 16 - Add your own datasets

The sample datasets are there so the server is useful the moment it boots; the point of ERDDAP is serving your data. Datasets are declared in /var/lib/erddap/content/erddap/datasets.xml. Add a <dataset> element for each one — EDDTableFromAsciiFiles for CSV, EDDTableFromNcFiles and EDDGridFromNcFiles for netCDF, EDDGridFromDap for a remote OPeNDAP server, and about forty other types — then reload:

sudo -e /var/lib/erddap/content/erddap/datasets.xml
sudo systemctl restart tomcat10

Upstream's own example catalogue is kept beside your file as datasets.xml.upstream for reference, and the full reference for every dataset type is in ERDDAP's datasets.xml documentation. ERDDAP also ships a GenerateDatasetsXml tool that inspects a data file and writes most of the XML for you.

Once a dataset is loaded, remove the sample ones by deleting their <dataset> elements, or set active="false" on them.

Maintenance

Add TLS. ERDDAP is served over plain HTTP on port 80 by default. For production, point a DNS name at the VM and terminate TLS in nginx, for example with Certbot, so the interface is served over HTTPS on port 443. ERDDAP builds the absolute URL of every link and image from the incoming Host header, which nginx passes through unchanged including any non standard port, so a correct proxy configuration needs no further change. If you front the server with something that rewrites the host, set <baseUrl> in setup.xml to the address your users actually use.

Tell ERDDAP who you are. /var/lib/erddap/content/erddap/setup.xml carries the administrator identity and institution that appear in your datasets' metadata and on the server's contact page. Edit the <admin...> values to your own organisation before you publish.

Enable email if you want it. The email settings in setup.xml are deliberately empty, so no SMTP credentials ship in the image and the server never tries to send mail. Fill in <emailSmtpHost>, <emailFromAddress> and <emailEverythingTo> if you want ERDDAP's daily reports and error notifications.

The subscription system is off. ERDDAP can let any caller register a URL that the server will fetch whenever a dataset changes. On a server with a public address that is a request forgery and traffic amplification vector aimed at your own network, so it is disabled here. If you need it, set <subscriptionSystemActive>true</subscriptionSystemActive> in setup.xml, configure email, and consider restricting who can reach the server.

Back up the data disk. Everything that matters lives on /var/lib/erddap: your datasets.xml and setup.xml, ERDDAP's cache and logs, and any data files you place there. Snapshot the disk to preserve all of it.

Memory. The JVM heap is set to half of the VM's physical memory on every first boot, which is upstream's recommendation, and is recorded as ERDDAP_HEAP_MB in the credentials file. If you resize the VM, the heap is not recalculated automatically — edit JAVA_OPTS in /etc/default/tomcat10 and restart tomcat10.service. ERDDAP's own memory guidance is in its server administration documentation.

Updates. The image ships with unattended security upgrades enabled for the operating system, and both Tomcat and the JDK come from Ubuntu's archive so they are covered by it. ERDDAP itself is upgraded by replacing the deployed web application with a newer erddap.war; your configuration and data on the data disk are untouched by that.

Support

Every cloudimg image is backed by 24/7 support. If you have any questions about deploying or operating ERDDAP on Azure, contact the cloudimg team.