ERDDAP
Overview
ERDDAP is a scientific data server from NOAA's NMFS SWFSC Environmental Research Division. It gives heterogeneous gridded and tabular datasets one consistent web interface and REST API, so a user or a script can request any subset of any dataset and receive it 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 NOAA, IOOS, EMODnet, CSIRO and IFREMER use it to publish their data.
This SecureAnyCloud image runs ERDDAP 2.30.0 from the official upstream release on Apache Tomcat 10.1 under OpenJDK 25. Tomcat and the JDK both come from Ubuntu's own package archive, so they keep receiving security updates on your VM. 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. The image ships three datasets whose data is already on the VM: the ETOPO1 global relief grid bundled inside ERDDAP itself, in both longitude conventions, and a small synthetic sample time series that demonstrates the tabular half of the server. Upstream's example configuration points at datasets on other people's servers, so a stock install shows an empty or broken list whenever those hosts are unreachable.
ERDDAP has no user database and no login of its own by design, because a data server's catalogue and data endpoints are meant to be public. This image fences off the administrative surface instead. A unique administrative password and a unique dataset flag key are generated on the first boot of every VM, and the pages that expose internal state sit behind that per-VM password.
What you get:
- ERDDAP 2.30.0 on Apache Tomcat 10.1 under OpenJDK 25, both from Ubuntu's archive so security updates keep flowing. The image does not use a hand-unpacked vendor JRE tarball, which would never be patched again.
- 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 and ready to subset and map
- Tomcat bound to loopback only and fronted by nginx on port 80, with unauthenticated
/healthzand/readyzprobes - The administrative pages (status, Prometheus metrics, dataset reload and subscriptions) behind a per-VM password
- A per-VM dataset flag key that replaces the example 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.serviceandnginx.serviceas enabled, active systemd units- 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 the recommended size. ERDDAP is a single JVM, and this image sets its heap 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), the size it is smoke-tested on. A server that holds a real catalogue and serves concurrent subset requests benefits from the extra headroom.
In the NSG, allow inbound 22/tcp from your management network and 80/tcp (plus 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 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).
Review the dedicated data disk on the Disks tab, then select Review + create and 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:erddap:default:latest
az vm create \
--resource-group <your-rg> \
--name erddap \
--image sac1647283583153:erddap:default:latest \
--size Standard_B2ms \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku StandardWhen the VM is created, open port 80 to reach the web interface:
az vm open-port --resource-group <your-rg> --name erddap --port 80 --priority 900Step 3 - Connect to your VM
Connect over SSH as azureuser:
ssh azureuser@<vm-public-ip>On first boot the VM resolves its public address, writes it into ERDDAP's configuration, generates the per-VM secrets and starts the server. This normally completes within a minute of the VM becoming reachable.
Step 4 - Confirm the services are running
ERDDAP runs inside Tomcat behind nginx. Confirm that both units are active and that the first-boot service completed:
systemctl is-active tomcat10.service nginx.service
systemctl is-active erddap-firstboot.service test -f /var/lib/sac/data/erddap-firstboot.done && echo "first boot completed"Tomcat is bound to the loopback connector only, so ERDDAP is reachable exclusively through the nginx front end. Confirm this 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. It also serves a readiness probe that is proxied straight through to ERDDAP, so it succeeds only once the server is 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 with a password generated for this VM and written to a root-only file at /root/erddap-credentials.txt (mode 0600). Read it with sudo:
sudo cat /root/erddap-credentials.txtConfirm that the administrative surface is closed without that password and opens 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 holds:
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', 'sacDemoStation'} <= ids, 'expected datasets are missing' print() print('%d datasets loaded' % len(rows)) "Step 8 - Request real data
Request three variables from the tabular dataset as CSV. ERDDAP returns a header row, a units row and the data:
curl -s 'http://localhost/erddap/tabledap/sacDemoStation.csv?time,station,sea_water_temperature,air_temperature' | head -14Now make the same request in JSON and check it properly. ERDDAP returns an error with HTTP 200, so parse the payload instead of trusting the status code:
curl -s -m 120 'http://localhost/erddap/tabledap/sacDemoStation.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 and .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/sacDemoStation.$ext?time,sea_water_temperature") sz=$(curl -s -o /dev/null -w '%{size_download}' -m 120 \
"http://localhost/erddap/tabledap/sacDemoStation.$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; } doneStep 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 that 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 on the server. Request a global topography and bathymetry map as a PNG, then confirm that the response 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. They are separate from the OS disk, and you can resize or snapshot the data disk independently. Confirm the layout:
df -h /var/lib/erddap | tail -2 ls -1 /var/lib/erddap echo "--- your configuration ---" ls -1 /var/lib/erddap/content/erddapStep 12 - Open ERDDAP in your browser
Browse to http://<vm-public-ip>/. ERDDAP's home page opens, the front door to the whole catalogue:
Step 13 - Browse the datasets
Choose List of All Datasets. Each dataset is listed with links to its gridded or tabular data access form, its Make A Graph page, its metadata and its source files:
Step 14 - Use the Data Access Form
Click data next to a dataset to open its Data Access Form. Use it to choose variables, set the range of each dimension and pick an output file format. The form then gives you a URL that you can paste into a script:
Step 15 - Draw maps and graphs in the browser
Click graph to open Make A Graph. The server renders the image. Use the form to change the graph type, the axes, the colour bar and the land mask, then redraw:
The same tool plots tabular data. Here the sample station's sea water temperature is plotted against time:
Choose .htmlTable as the file type instead to see the data itself in the browser, with the units under each variable name:
Step 16 - Add your own datasets
The sample datasets make the server useful the moment it boots, but the point of ERDDAP is serving your own data. Datasets are declared in /var/lib/erddap/content/erddap/datasets.xml.
Add a <dataset> element for each one. Use 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 tomcat10Upstream's own example catalogue is kept beside your file as datasets.xml.upstream for reference. 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 your own 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. nginx passes that header 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. To receive ERDDAP's daily reports and error notifications, fill in <emailSmtpHost>, <emailFromAddress> and <emailEverythingTo>.
The subscription system is off. ERDDAP can let any caller register a URL that the server fetches 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. On every first boot, the JVM heap is set to half of the VM's physical memory, which is upstream's recommendation, and 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. Tomcat and the JDK come from Ubuntu's archive, so those upgrades cover them too. To upgrade ERDDAP itself, replace the deployed web application with a newer erddap.war. Your configuration and data on the data disk are untouched.
Support
Every SecureAnyCloud image is backed by 24/7 support. If you have any questions about deploying or operating ERDDAP on Azure, contact the SecureAnyCloud team.
Deploy on Azure
Launch ERDDAP 2.30.0 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