ERDDAP VM Setup
This chapter configures a dedicated ERDDAP serving VM after the infrastructure has been created. A file-backed deployment may need a high-memory VM, a large persistent cache disk, and a GCS data bucket. An ERDDAP that mainly serves remote sources can often use a smaller VM and omit the bucket mount.
The examples use these neutral local conventions:
| Item | Example used in this guide |
|---|---|
| Shared administration group | erddap-admins |
| Persistent data disk | /dev/disk/by-id/google-erddap-data |
| Persistent data mount | /mnt/disks/erddap-data |
| Optional GCS mount | /mnt/gcs/erddap-data |
These names are not GCP requirements. A team may change them, but the Terraform device name, mount configuration, installer variables, file ownership, and systemd units must remain consistent.
1. Configure the Operating System
1.2 Initialize and mount the persistent data disk
Confirm the stable device path before continuing. The following block formats the device only when blkid finds no existing filesystem.
ERDDAP_ADMIN_GROUP=erddap-admins
data_device=/dev/disk/by-id/google-erddap-data
data_mount=/mnt/disks/erddap-data
ls -l /dev/disk/by-id/
if sudo blkid "$data_device" >/dev/null 2>&1; then
echo "Existing filesystem detected; the disk will not be formatted."
sudo blkid "$data_device"
else
echo "No filesystem detected; formatting the new data disk."
sudo mkfs.ext4 \
-m 0 \
-E lazy_itable_init=0,lazy_journal_init=0,discard \
"$data_device"
fi
sudo mkdir -p "$data_mount"
sudo mount -o discard,defaults "$data_device" "$data_mount"
disk_uuid=$(sudo blkid -s UUID -o value "$data_device")
test -n "$disk_uuid" || {
echo "Unable to determine the data-disk UUID" >&2
exit 1
}
sudo cp -a /etc/fstab "/etc/fstab.before-erddap-$(date +%Y%m%d-%H%M%S)"
if ! grep -qE "^[[:space:]]*UUID=${disk_uuid}[[:space:]]" /etc/fstab; then
echo "UUID=${disk_uuid} ${data_mount} ext4 defaults,discard 0 2" |
sudo tee -a /etc/fstab
fi
sudo chown root:root /mnt/disks
sudo chmod 0755 /mnt/disks
sudo chown root:"$ERDDAP_ADMIN_GROUP" "$data_mount"
sudo chmod 2775 "$data_mount"
if sudo test -e /data || sudo test -L /data; then
echo "/data already exists; review it before changing the path."
else
sudo ln -s "$data_mount" /data
fiValidate both the mount and /etc/fstab before rebooting:
namei -l /mnt/disks/erddap-data
findmnt /mnt/disks/erddap-data
df -hT /mnt/disks/erddap-data
sudo umount /mnt/disks/erddap-data
sudo mount -a
findmnt /mnt/disks/erddap-data1.3 Optionally mount a GCS data bucket with rclone
Complete this subsection only when ERDDAP will read file-backed datasets from a GCS bucket. Skip it for an ERDDAP that uses only remote HTTP, THREDDS, other ERDDAP servers, or other network sources.
The VM service account needs permission to list and read objects in the selected bucket. The infrastructure example grants roles/storage.objectViewer at bucket scope.
Install rclone and permit FUSE --allow-other mounts:
curl -fsSL https://rclone.org/install.sh | sudo bash
if ! sudo grep -qx 'user_allow_other' /etc/fuse.conf; then
echo user_allow_other | sudo tee -a /etc/fuse.conf
fiThis intentionally installs rclone’s current stable release. Environments requiring fully pinned builds should select and verify a specific release.
Create a root-owned configuration for the systemd service:
ERDDAP_ADMIN_GROUP=erddap-admins
sudo install -d -o root -g "$ERDDAP_ADMIN_GROUP" -m 0750 /etc/rclone
sudo tee /etc/rclone/rclone.conf >/dev/null <<'EOF'
[gcs-remote]
type = google cloud storage
env_auth = true
project_number = YOUR_GCP_PROJECT_NUMBER
location = YOUR_GCP_REGION
EOF
sudo chown root:"$ERDDAP_ADMIN_GROUP" /etc/rclone/rclone.conf
sudo chmod 0640 /etc/rclone/rclone.confTest identity and IAM before configuring the mount:
sudo rclone lsd \
--config /etc/rclone/rclone.conf \
gcs-remote:YOUR_ERDDAP_BUCKET_NAMECreate the mount and cache directories:
ERDDAP_ADMIN_GROUP=erddap-admins
sudo mkdir -p /mnt/gcs/erddap-data
sudo mkdir -p /mnt/disks/erddap-data/rclone-cache
sudo chown root:root /mnt/gcs
sudo chmod 0755 /mnt/gcs
sudo chown root:"$ERDDAP_ADMIN_GROUP" /mnt/gcs/erddap-data
sudo chmod 2775 /mnt/gcs/erddap-dataCreate the persistent mount service. Set the rclone cache limit based on the size of the VM’s data disk and expected workload. 100G is an example, not a recommended limit for every deployment. A large ERDDAP installation may use a limit such as 2000G. Leave enough disk space outside the cache for ERDDAP working files, logs, and growth.
ERDDAP_ADMIN_GROUP=erddap-admins
RCLONE_CACHE_MAX_SIZE=100G # Example; adjust for your data disk
FUSERMOUNT_BIN=$(command -v fusermount3 || command -v fusermount)
test -n "$FUSERMOUNT_BIN" || {
echo "fusermount is not installed" >&2
exit 1
}
sudo tee /etc/systemd/system/rclone-mount.service >/dev/null <<EOF
[Unit]
Description=Read-only GCS mount for ERDDAP
After=network-online.target
Wants=network-online.target
RequiresMountsFor=/mnt/disks/erddap-data
[Service]
Type=notify
User=root
ExecStart=/usr/bin/rclone mount gcs-remote:YOUR_ERDDAP_BUCKET_NAME /mnt/gcs/erddap-data \
--config /etc/rclone/rclone.conf \
--allow-other \
--read-only \
--dir-cache-time 1h \
--attr-timeout 1h \
--vfs-cache-mode full \
--vfs-cache-max-age 48h \
--vfs-cache-max-size ${RCLONE_CACHE_MAX_SIZE} \
--vfs-cache-poll-interval 1m \
--cache-dir /mnt/disks/erddap-data/rclone-cache \
--buffer-size 128M \
--vfs-read-ahead 256M \
--vfs-read-chunk-size 64M \
--vfs-read-chunk-size-limit 2G \
--dir-perms 0775 \
--file-perms 0664 \
--gid $(getent group "$ERDDAP_ADMIN_GROUP" | cut -d: -f3) \
--umask 002 \
--rc \
--rc-no-auth
ExecStop=${FUSERMOUNT_BIN} -u /mnt/gcs/erddap-data
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now rclone-mount.serviceThe example values are a tested starting point, not universal tuning. In particular, --attr-timeout controls how long the kernel caches file attributes; it is not an interval for polling GCS for replaced objects. Measure performance and freshness requirements before changing these values.
Verify the service and mount:
systemctl is-active rclone-mount.service
findmnt /mnt/gcs/erddap-data
ls -la /mnt/gcs/erddap-data | head
rclone rc vfs/stats2. Install and Configure ERDDAP
2.1 Run the installation script
The repository script installs pinned Java, Tomcat, ERDDAP, and erddapContent versions. It configures the tomcat-erddap service, persistent working directory, and reverse-proxy handling. It refuses to overwrite an existing Java or Tomcat installation and is therefore intended for a new host, not an in-place upgrade.
Check physical memory before choosing the Java heap:
free -hSet the heap to approximately 50–70% of physical RAM and never above 80%, leaving memory for the operating system, Tomcat native memory, rclone, and filesystem cache.
wget -O erddap_setup.sh \
https://raw.githubusercontent.com/erd-coastwatch/swfsc-gcp-migration/main/scripts/erddap/erddap_setup.sh
chmod 700 erddap_setup.sh
bash -n erddap_setup.sh
./erddap_setup.sh --help
TOMCAT_GROUP=erddap-admins \
JAVA_HEAP_MB=32768 \
./erddap_setup.shThe example heap is 32 GB; smaller VMs must use a smaller value. The installer enables the service but leaves it stopped so configuration can be reviewed first.
The SWFSC script currently pins Tomcat 11 with ERDDAP 2.31.1 because that combination was validated on a new VM. Treat any version change as a planned, tested upgrade.
Make Tomcat require the persistent disk:
sudo mkdir -p /etc/systemd/system/tomcat-erddap.service.d
sudo tee \
/etc/systemd/system/tomcat-erddap.service.d/storage.conf \
>/dev/null <<'EOF'
[Unit]
RequiresMountsFor=/mnt/disks/erddap-data
EOFFor a bucket-backed deployment, also require the rclone service:
sudo tee \
/etc/systemd/system/tomcat-erddap.service.d/rclone.conf \
>/dev/null <<'EOF'
[Unit]
Requires=rclone-mount.service
After=rclone-mount.service
EOF
sudo systemctl daemon-reloadSkip the rclone override when no bucket is mounted.
2.2 Configure ERDDAP
Follow the official ERDDAP installation documentation and review at least:
bigParentDirectory;baseUrlandbaseHttpsUrl;- administrator and institution information;
- email, subscription, and authentication settings;
flagKeyKey;- the initial
datasetsRegex.
sudoedit /usr/local/tomcat_erddap/content/erddap/setup.xmlWhen ERDDAP is behind an HTTPS load balancer, both public URL settings should use the external hostname:
<baseUrl>https://YOUR_ERDDAP_HOSTNAME</baseUrl>
<baseHttpsUrl>https://YOUR_ERDDAP_HOSTNAME</baseHttpsUrl>The validated installer content includes the small etopo180 test dataset. Confirm that the installed catalog still contains it before using that ID:
catalog=/usr/local/tomcat_erddap/content/erddap/datasets.xml
if grep -q 'datasetID="etopo180"' "$catalog"; then
echo "etopo180 is available for the initial test."
else
echo "Select a small dataset ID that is present in the installed catalog."
grep -oE 'datasetID="[^"]+"' "$catalog" | head
fiRestrict datasetsRegex to the selected small dataset for the first start. For the validated content package:
<datasetsRegex>etopo180</datasetsRegex>The installer also adds Tomcat’s RemoteIpValve, allowing Tomcat to honor the load balancer’s forwarded protocol and client-address headers. The user does not need to paste a valve into server.xml; verify it only when troubleshooting proxy behavior:
sudo grep -A4 -B1 'RemoteIpValve' \
/usr/local/tomcat_erddap/conf/server.xmlSet configuration ownership and validate the XML:
sudo chown tomcat:erddap-admins \
/usr/local/tomcat_erddap/content/erddap/setup.xml \
/usr/local/tomcat_erddap/content/erddap/datasets.xml
sudo chmod 0640 \
/usr/local/tomcat_erddap/content/erddap/setup.xml \
/usr/local/tomcat_erddap/content/erddap/datasets.xml
sudo xmllint --noout /usr/local/tomcat_erddap/content/erddap/setup.xml
sudo xmllint --noout /usr/local/tomcat_erddap/content/erddap/datasets.xml
sudo xmllint --noout /usr/local/tomcat_erddap/conf/server.xml3. Validate ERDDAP in Layers
Complete each layer before troubleshooting the next one.
Layer 1: local service
On the ERDDAP VM:
sudo systemctl start tomcat-erddap
sudo systemctl --no-pager --full status tomcat-erddap
curl -fsSI http://127.0.0.1:8080/erddap/status.html
curl -fsSI http://127.0.0.1:8080/erddap/index.html
curl -fsSI http://127.0.0.1:8080/erddap/griddap/etopo180.htmlUse the selected test dataset ID in the final request if it is not etopo180. Do not proceed until the local requests succeed.
Layer 2: private network access
From an authorized host on the VPC, or through the SSH forwarding method described in Accessing VMs:
curl -fsSI http://YOUR_ERDDAP_INTERNAL_IP:8080/erddap/status.htmlFailure here, after Layer 1 succeeds, points to host or VPC access rather than ERDDAP configuration.
Layer 3: load-balancer backend
This guide assumes the responsible infrastructure team has created or will create the instance group, named port 8080, backend service, /erddap/status.html health check, URL map, HTTPS proxy, certificate, forwarding rule, and DNS or organizational external route. Record which team and Terraform state own each shared component.
The backend must accept TCP 8080 from Google’s health-check ranges and, for a regional Application Load Balancer, the region’s proxy-only subnet. Determine the proxy-only range rather than copying one from another project:
gcloud compute networks subnets list \
--project=YOUR_GCP_PROJECT_ID \
--filter='purpose=REGIONAL_MANAGED_PROXY' \
--format='table(name,region,network,ipCidrRange,purpose,role)'Create narrowly scoped VPC firewall rules through the Terraform state or administrative process that owns networking. Do not expose Tomcat port 8080 to the public internet.
If the VM image also uses UFW, allow the same health-check and proxy sources locally:
sudo ufw allow proto tcp from 130.211.0.0/22 \
to YOUR_ERDDAP_INTERNAL_IP port 8080 comment 'Google LB health checks'
sudo ufw allow proto tcp from 35.191.0.0/16 \
to YOUR_ERDDAP_INTERNAL_IP port 8080 comment 'Google LB health checks'
sudo ufw allow proto tcp from YOUR_PROXY_ONLY_SUBNET \
to YOUR_ERDDAP_INTERNAL_IP port 8080 comment 'Regional LB proxy subnet'
sudo ufw status numberedFrom Cloud Shell or an administrative workstation, confirm backend health:
gcloud compute backend-services get-health YOUR_BACKEND_SERVICE \
--project=YOUR_GCP_PROJECT_ID \
--region=YOUR_GCP_REGIONProceed when the backend reports HEALTHY.
Layer 4: public HTTPS route
curl -fsSI https://YOUR_ERDDAP_HOSTNAME/erddap/status.html
curl -fsS https://YOUR_ERDDAP_HOSTNAME/erddap/index.html |
grep -oE '<link[^>]*erddap2.css[^>]*>'The status request should return HTTP 200, and generated asset URLs should use the public HTTPS hostname. Production checks should validate the complete certificate chain.
Troubleshooting logs
Consult logs when a validation layer fails rather than following them as an unrelated setup step:
sudo journalctl -u tomcat-erddap --since '15 minutes ago'
sudo tail -n 200 \
/mnt/disks/erddap-data/erddap/logs/log.txt4. Perform the Reboot Test
After all four validation layers succeed:
sudo rebootReconnect and verify automatic recovery:
findmnt /mnt/disks/erddap-data
# Bucket-backed deployments only
findmnt /mnt/gcs/erddap-data
systemctl is-active rclone-mount.service
systemctl is-active tomcat-erddap.service
curl -fsSI http://127.0.0.1:8080/erddap/status.htmlRepeat the backend-health and public-HTTPS checks. The infrastructure setup is complete when the service recovers automatically after reboot.
The ERDDAP administrator can then manage the production datasets.xml using the official datasets.xml documentation and the team’s dataset-specific procedures. Production catalog authoring and dataset migration are intentionally outside this infrastructure guide.
To also use the ERDDAP hostname as an entry point to a GitHub Pages website, continue to Optional Website Redirect with Apache. This integration is optional and follows the validated ERDDAP setup above.
Operational Reference: rclone Cache Refresh
This subsection applies only to bucket-backed deployments after setup is complete.
Objects added or replaced directly in GCS may remain hidden until rclone’s directory or VFS caches expire. Refresh only the affected directory when practical:
rclone rc vfs/refresh \
dir=PATH/RELATIVE/TO/BUCKET \
recursive=trueWhen one same-name object must be reread, forget that cached path:
rclone rc vfs/forget \
file=PATH/RELATIVE/TO/BUCKET/file.ncThe VFS cache-size limit is a ceiling, not a disk reservation, and open files can temporarily prevent eviction. Monitor both cache statistics and free disk space:
rclone rc vfs/stats
df -h /mnt/disks/erddap-data