THREDDS Data Server on Ubuntu 24.04 on Azure User Guide
Overview
The THREDDS Data Server (TDS) is the reference server from NSF Unidata for publishing scientific data. You point it at a directory of netCDF, HDF, GRIB or NEXRAD files and it exposes every one of them through the remote access protocols the earth science community already uses: OPeNDAP and DAP4 for programmatic subsetting from Python, R, MATLAB, Panoply or the netCDF C library; OGC WMS for map layers a GIS client can consume; OGC WCS for coverage retrieval; the NetCDF Subset Service for pulling a bounding box or a time range straight out of a large grid; and plain HTTP for whole file download.
What ties it together is the THREDDS catalog, an XML description of the holdings that clients crawl to discover what is available and how to reach it. Catalogs can be written by hand for curated collections or generated automatically by scanning a directory, and the server can aggregate many files into a single logical dataset so a whole model run series appears to a client as one continuous time axis.
This image runs THREDDS Data Server 5.9 from the official Unidata release on Apache Tomcat 10.1 under OpenJDK 17, both taken from Ubuntu's own package archive so they keep receiving security updates on your VM. That pairing is not incidental: TDS 5.9 is a Jakarta Servlet application compiled to Java 17 bytecode, so it requires Tomcat 10 or later and refuses to load on Tomcat 9, and Java 17 is the exact runtime NSF Unidata's own continuous integration tests it against.
The catalogue works the moment the VM boots, with no external service involved. Nine small sample netCDF files ship inside the server's own distribution and this image publishes them: a global ocean wind and wave forecast grid, four NCEP Eta model runs on the AWIPS 211 grid, a deliberately awkward polar stereographic grid, and two station time series in the CF discrete sampling geometry convention. Nothing in the shipped catalogue points at a third party server, so the appliance serves real data on a VM with no internet access at all.
A unique administrator password and a unique TLS certificate are generated on the first boot of every VM. A data server's catalogue and data endpoints are meant to be public — that is the product. What this image fences off is the management surface: /thredds/admin/ is refused outright over plain HTTP and is reachable only over HTTPS, and only to an account whose password exists on your VM and nowhere else. Backed by 24/7 cloudimg support.
What is included:
- THREDDS Data Server 5.9 on Apache Tomcat 10.1 under OpenJDK 17, both from Ubuntu's archive so security updates keep flowing
- A working catalogue on first boot with nine locally served sample datasets and no dependency on any third party server
- OPeNDAP, DAP4, OGC WMS, OGC WCS, the NetCDF Subset Service, CdmRemote and HTTP file download, all enabled
- Godiva3, the built in WMS map viewer, opening on the sample data rather than an empty layer tree
- Per variable WMS colour scales, so a plain GetMap from QGIS or a Leaflet layer returns a readable map instead of a flat rectangle
- Tomcat bound to the loopback interface only, behind an nginx reverse proxy that forwards an explicit allowlist and answers everything else with a 404
- The management surface refused over plain HTTP and behind a per VM administrator account over HTTPS
- No Tomcat manager or host-manager webapp anywhere in the image, and no users or roles in
tomcat-users.xmluntil first boot writes yours - Remote catalogue fetching, remote WMS and remote WCS all disabled, so the server never fetches a caller supplied URL
- The version phone-home switched off, so the appliance makes no unsolicited outbound call
- A JVM heap sized from the VM's own memory on every first boot, so one image is correct on any VM size
- Ubuntu 24.04 LTS fully patched at capture with unattended security upgrades enabled
Prerequisites
- An Azure subscription with permission to create virtual machines
- An SSH key pair for the
azureuseraccount - A network security group allowing inbound TCP 22, 80 and 443 from the addresses you will use
Step 1 - Deploy from the Azure Marketplace
In the Azure portal, search the Marketplace for THREDDS Data Server 5.9 on Ubuntu 24.04 LTS by cloudimg, select Create, and choose a size of Standard_B2s or larger. Set the administrator username to azureuser and supply your SSH public key. On the Networking tab allow inbound SSH (22), HTTP (80) and HTTPS (443).
Step 2 - Deploy from the Azure CLI
az group create --name thredds-rg --location eastus
az vm create \
--resource-group thredds-rg \
--name thredds-server \
--image cloudimg:thredds-data-server:default:latest \
--size Standard_B2s \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
az vm open-port --resource-group thredds-rg --name thredds-server --port 22 --priority 1001
az vm open-port --resource-group thredds-rg --name thredds-server --port 80 --priority 1002
az vm open-port --resource-group thredds-rg --name thredds-server --port 443 --priority 1003
First boot takes about a minute: a service generates this VM's administrator password and TLS certificate, sizes the JVM heap from the VM's memory, and only then releases Tomcat and nginx to start.
Step 3 - Connect to your VM
ssh azureuser@<vm-ip>
Step 4 - Confirm the services are running
for unit in thredds-firstboot tomcat10 nginx; do
printf '%-26s %s\n' "$unit" "$(systemctl is-active "$unit")"
done
tomcat10 and nginx report active. thredds-firstboot is a one-shot unit that reports active once it has done its work, because it is declared RemainAfterExit=yes — that is how systemd remembers this VM has already been configured. For the full picture:
systemctl --no-pager --lines=0 status tomcat10 nginx

The two application services are deliberately held back until first boot has finished. Each carries a systemd drop-in with ConditionPathExists on a marker file that first boot writes as its very last action, so there is no window in which the server is running before this VM's own credentials exist:
systemctl cat tomcat10.service | grep -E 'ConditionPathExists|After=thredds'
Step 5 - Confirm the server is answering
curl -s -o /dev/null -w 'catalogue: HTTP %{http_code}\n' http://127.0.0.1/thredds/catalog/catalog.xml
curl -s -o /dev/null -w 'health: HTTP %{http_code}\n' http://127.0.0.1/healthz
curl -s -o /dev/null -w 'readiness: HTTP %{http_code}\n' http://127.0.0.1/readyz
/healthz answers as soon as nginx is up. /readyz is proxied through to TDS itself, so it only succeeds once the server is genuinely serving its catalogue — that is the one to use for a load balancer probe.
Step 6 - Browse the shipped catalogue
curl -s http://127.0.0.1/thredds/catalog/catalog.xml | grep -oE '<dataset name="[^"]+" ID="[^"]+"' | head -12

Every dataset above resolves to a file already on this VM. The catalogue is at /var/lib/thredds/content/thredds/catalog.xml and the data it points at is under /var/lib/thredds/content/thredds/public/testdata/.
Step 7 - Retrieve your administrator password
The management surface is the only part of the server that is not public, and its credentials are generated on this VM alone:
sudo cat /root/thredds-credentials.txt
The file is 0600 root:root and contains THREDDS_ADMIN_USER, THREDDS_ADMIN_PASSWORD, the URL of the administration pages and the SHA-256 fingerprint of this VM's TLS certificate. The password is stored in Tomcat as a salted, 200,000-iteration SHA-512 digest; the plaintext exists only in this file.
Step 8 - Confirm the management surface is closed
curl -s -o /dev/null -w 'admin over http: HTTP %{http_code} (403 = refused)\n' http://127.0.0.1/thredds/admin/debug
curl -sk -o /dev/null -w 'admin over https, no creds: HTTP %{http_code} (401 = challenged)\n' https://127.0.0.1/thredds/admin/debug
curl -s -o /dev/null -w 'tomcat manager webapp: HTTP %{http_code} (404 = does not exist)\n' http://127.0.0.1/manager/html
Plain HTTP refuses the management paths outright rather than redirecting, so an administrator password can never cross the network in the clear. Now authenticate over HTTPS with the password from Step 7:
curl -sk -o /dev/null -w 'admin with credentials: HTTP %{http_code}\n' \
-u "tdsadmin:<THREDDS_ADMIN_PASSWORD>" https://127.0.0.1/thredds/admin/debug

The certificate is self signed and was generated on this VM, so its private key exists nowhere else — that is why curl needs -k and why a browser will warn on first visit. Step 15 covers installing your own certificate.
Step 9 - Pull real data over OPeNDAP
OPeNDAP is how most scientific clients talk to a THREDDS server. Ask for the dataset descriptor first, then a subset of actual values:
curl -s http://127.0.0.1/thredds/dodsC/cloudimg/ocean.nc.dds
curl -s 'http://127.0.0.1/thredds/dodsC/cloudimg/ocean.nc.ascii?wave_hgt_sfc%5B0:1:0%5D%5B30:1:33%5D%5B30:1:33%5D'

Those are significant wave heights in metres from a 4 by 4 corner of the forecast grid. The same URL works from Python:
python3 -c "print('pip install netCDF4, then: netCDF4.Dataset(\"http://<vm-ip>/thredds/dodsC/cloudimg/ocean.nc\")')"
Step 10 - Subset a grid with the NetCDF Subset Service
NCSS has two shapes. A grid subset returns a netCDF file:
curl -s -o /tmp/subset.nc \
'http://127.0.0.1/thredds/ncss/grid/cloudimg/ocean.nc?var=wave_hgt_sfc&horizStride=8&temporal=all&accept=netcdf'
ls -l /tmp/subset.nc && head -c 3 /tmp/subset.nc && echo ' <- netCDF classic magic'
A grid-as-point request returns a time series at one location as CSV:
curl -s 'http://127.0.0.1/thredds/ncss/grid/cloudimg/ocean.nc?var=wave_hgt_sfc&latitude=20&longitude=-40&time=all&accept=csv'
Note that grid subsets are netCDF only; asking a grid subset for CSV returns HTTP 400. Text output comes from the point shape, as above, or from the point service over the station samples:
curl -s 'http://127.0.0.1/thredds/ncss/point/cloudimg/H.2.1.1.nc?var=humidity&accept=csv&temporal=all' | head -8
Step 11 - Render a map with OGC WMS
curl -s 'http://127.0.0.1/thredds/wms/cloudimg/ocean.nc?service=WMS&version=1.3.0&request=GetCapabilities' | grep -oE '<Name>[^<]+</Name>' | head -8
curl -s -o /tmp/wave.png \
'http://127.0.0.1/thredds/wms/cloudimg/ocean.nc?service=WMS&version=1.3.0&request=GetMap&layers=wave_hgt_sfc&styles=&format=image/png&transparent=false&crs=CRS:84&bbox=-180,-90,180,90&width=512&height=256'
file /tmp/wave.png
That request carries no colorscalerange, which is what a GIS client sends by default. This image gives every shipped variable a colour scale range matched to its real physical range in /var/lib/thredds/content/thredds/wmsConfig.xml, so the default request returns a readable map. Add a block there for your own variables when you publish them, or the same request will come back as one flat colour.
Step 12 - Download a whole file
curl -s -o /tmp/ocean.nc http://127.0.0.1/thredds/fileServer/cloudimg/ocean.nc
sha256sum /tmp/ocean.nc /var/lib/thredds/content/thredds/public/testdata/ocean.nc
The two checksums match: the HTTP file service hands back the bytes on disk untouched.
Step 13 - Open the server in your browser
Browse to http://<vm-ip>/thredds/catalog/catalog.html. The catalogue lists every shipped dataset.

Select a dataset to see every access method it offers, with the live URL for each one.

Step 14 - Draw maps in Godiva3 and subset in the browser
Godiva3 is the WMS viewer built into TDS. Browse to http://<vm-ip>/thredds/Godiva.html and it opens on the sample ocean dataset; expand the dataset in the tree on the left and pick a variable.

Godiva3 draws its background map from a public tile service on the internet, so on a VM with no outbound access the data layer renders over a blank background. The data itself is served entirely from your VM.
The NetCDF Subset Service also has a browser form, at the NetcdfSubset link on any grid dataset page, where you can pick variables, drag a bounding box and choose an output format.

Step 15 - Make the server your own
Four things are worth changing before you publish this server.
Identify your organisation. TDS renders these on every generated page and in the metadata clients harvest:
grep -A 12 '<serverInformation>' /var/lib/thredds/content/thredds/threddsConfig.xml
Edit /var/lib/thredds/content/thredds/threddsConfig.xml with your editor of choice, then restart:
sudo systemctl restart tomcat10
Install a real TLS certificate. Replace the self signed pair and reload nginx:
ls -l /etc/ssl/cloudimg-thredds/
Put your certificate at /etc/ssl/cloudimg-thredds/server.crt and its key at /etc/ssl/cloudimg-thredds/server.key (key mode 0600 root:root), then sudo systemctl reload nginx.
Add a data disk for real collections. The shipped samples live on the OS disk, which is fine for evaluation. For a real archive, attach an Azure data disk, format and mount it with a UUID keyed nofail entry in /etc/fstab, and point a datasetScan at it.
Publish your own data. Drop files into the scanned directory and they appear in the catalogue with every service already attached:
ls /var/lib/thredds/content/thredds/public/testdata/
curl -s http://127.0.0.1/thredds/catalog/cloudimgAll/catalog.xml | grep -c 'urlPath'
For a curated catalogue, edit /var/lib/thredds/content/thredds/catalog.xml and add dataset or datasetScan entries, then reload the configuration from the management pages or restart Tomcat. The full catalog specification is in the THREDDS documentation.
Maintenance
The JVM heap is sized from the VM's memory on first boot. On a Standard_B2s that is 1467 MB, measured under real query load with roughly 2.7 GB still free and no swap in use. If you move to a larger VM size the heap follows automatically on that VM's first boot; to change it later, edit JAVA_OPTS in /etc/default/tomcat10 and restart Tomcat.
grep -oE '\-Xm[sx][0-9]+m' /etc/default/tomcat10
free -m | head -2
TDS writes its own logs under the content root, and they are rotated weekly:
sudo ls -l /var/lib/thredds/content/thredds/logs/
Ubuntu security updates are applied automatically:
grep -E 'Unattended-Upgrade|Update-Package-Lists' /etc/apt/apt.conf.d/20auto-upgrades
To upgrade TDS itself, download the newer thredds-<version>.war from Unidata, verify it against the published .sha256, and unpack it over /var/lib/tomcat10/webapps/thredds/. Your configuration lives outside the webapp under /var/lib/thredds/content/, so it survives the upgrade untouched.
Support
This image is maintained by cloudimg with 24/7 support. The THREDDS Data Server is released by NSF Unidata under the BSD 3-Clause licence; the full text ships at /usr/share/doc/thredds-data-server/LICENSE, alongside a note recording the exact upstream artifact and its checksum.
head -4 /usr/share/doc/thredds-data-server/LICENSE
cat /usr/share/doc/thredds-data-server/README.cloudimg