Deployment guide

Snowstorm

Overview

Snowstorm is the official open-source SNOMED CT terminology server developed by SNOMED International, built on Elasticsearch for performance and enterprise scale. It serves SNOMED CT clinical terminology through a REST API and a standards-based HL7 FHIR terminology API. It supports concept lookup, description and full-text search, the SNOMED Expression Constraint Language (ECL) for querying subsets of the hierarchy, reference set membership, and the FHIR CodeSystem, ValueSet and ConceptMap operations.

Clinical systems, terminology browsers and data pipelines use Snowstorm as an authoritative, queryable SNOMED CT service. The SecureAnyCloud image ships the full server stack wired together and ready on the first boot of every VM. The server ships healthy and empty, because SNOMED CT content requires your own licence. You import your licensed release when you are ready.

What you get:

  • Snowstorm 10.11.2 (Apache 2.0) served on port 80 through nginx, managed by systemd, with the Swagger interface at the site root
  • Elasticsearch 8.11.1 as the backing datastore, single node, bound to 127.0.0.1 only. It is the exact Elasticsearch version Snowstorm is tested against.
  • Eclipse Temurin 17 as the Java runtime
  • A per-VM Elasticsearch superuser password, generated on first boot and recorded in a root-only file, with no shipped default credential
  • A read-only API by default, so no unauthenticated caller can alter content or run mutating and administrative operations
  • A hardened Ubuntu 24.04 LTS base, patched at build time
  • Java heaps tuned for the recommended 8 GiB VM (Elasticsearch 3 GiB, Snowstorm 2 GiB)
  • elasticsearch.service, snowstorm.service, nginx.service and snowstorm-firstboot.service as systemd units
  • An unauthenticated /healthz 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 plus subnet in the target region.

Standard_B2ms (2 vCPU / 8 GiB RAM) is the recommended size and is what the image is tuned for. Importing and querying the full SNOMED CT International Edition is memory intensive. For production content, SNOMED International recommends at least 8 GiB for Elasticsearch alone, so size up before importing a full release. For example, use a memory-optimised Standard_E4s_v5 with 32 GiB.

In the NSG, allow inbound 22/tcp from your management network and 80/tcp for the API. Snowstorm serves plain HTTP on port 80, so for production, put your own domain and TLS in front of it. Elasticsearch is never exposed. It listens on 127.0.0.1:9200 only, and Snowstorm listens on 127.0.0.1:8080 only, so neither port is reachable from the network.

You also need your own SNOMED CT licence and RF2 release files to load real content. SNOMED International licenses SNOMED CT, and in many countries it is available at no charge through a national release centre. See https://www.snomed.org/get-snomed. The SecureAnyCloud image ships with no SNOMED CT content.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Snowstorm by SecureAnyCloud, 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). Then choose Review + create, followed by 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:snowstorm-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name snowstorm \
  --image sac1647283583153:snowstorm-ubuntu-24-04:default:latest \
  --size Standard_B2ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Then open port 80 to the API:

az vm open-port --resource-group <your-rg> --name snowstorm --port 80

Step 3 - Confirm the services are running

SSH in as azureuser and confirm that Elasticsearch, Snowstorm and nginx are all active.

The snowstorm-firstboot unit is a one-shot unit. It runs once on the first boot to create the per-VM Elasticsearch password and start the services, then stays satisfied. Elasticsearch and Snowstorm listen only on the loopback interface, while nginx serves the API on port 80.

systemctl is-active elasticsearch snowstorm nginx
active active active
ss -tlnp | grep -E ':80 |:8080 |:9200 '

The listing confirms nginx on 0.0.0.0:80 (public), with Elasticsearch on 127.0.0.1:9200 and Snowstorm on 127.0.0.1:8080 bound to the loopback interface only. The Java runtime is Eclipse Temurin 17.

Step 4 - Retrieve the per VM credentials

The first boot of every VM generates a unique Elasticsearch superuser password and writes it to a root-only file. Read it with sudo:

sudo cat /root/snowstorm-credentials.txt
SNOWSTORM_URL=http://<your-vm-ip>/ SNOWSTORM_FHIR_URL=http://<your-vm-ip>/fhir ES_ELASTIC_PASSWORD=<generated-on-first-boot> SNOWSTORM_SPRING_PASSWORD=<generated-on-first-boot>

The passwords are unique to your VM. You do not need them for day-to-day read-only API use, which is open on port 80. They secure the Elasticsearch datastore and are there if you enable Snowstorm's authenticated administrative access.

Step 5 - Explore the Swagger interface

Open http://<your-vm-ip>/ in a browser. Snowstorm serves an interactive Swagger (OpenAPI 3.0) interface that documents every endpoint of the terminology server and lets you call them.

The image ships in read-only mode, so operations that change content and administrative operations are disabled and hidden. What you see is the safe read and query surface.

Step 6 - Query the REST API

The REST API answers on port 80. Check the server version:

curl -s http://127.0.0.1/version

List the code systems. On a freshly deployed image, the root SNOMEDCT code system exists on the MAIN branch but holds no content yet. This empty but healthy state is expected before you import a release:

curl -s http://127.0.0.1/codesystems

Once you have imported a SNOMED CT release, the same API answers concept, description and ECL queries. For example, use http://<your-vm-ip>/MAIN/concepts?term=asthma for a term search or http://<your-vm-ip>/MAIN/concepts?ecl=<404684003 for an Expression Constraint Language query.

Step 7 - Use the HL7 FHIR terminology API

Snowstorm also exposes a standards-based HL7 FHIR terminology API under /fhir. It provides the CodeSystem, ValueSet and ConceptMap resources and operations such as $lookup, $expand, $validate-code, $subsumes and $translate. After you import a release, these operations run against your licensed SNOMED CT content. For example:

http://<your-vm-ip>/fhir/CodeSystem/$lookup?system=http://snomed.info/sct&code=427623005 http://<your-vm-ip>/fhir/ValueSet/$expand?url=http://snomed.info/sct?fhir_vs=isa/404684003

Step 8 - Import your licensed SNOMED CT release

The server ships empty and read-only. To load real content, provide your own licensed SNOMED CT RF2 release and run a one-time import. Snowstorm's recommended flow is to import while the server is writable, then return it to read-only for serving.

First, obtain the SNAPSHOT RF2 archive for your edition from SNOMED International or your national release centre, and copy it to the VM. Then switch Snowstorm to writable and restart it:

sudo sed -i 's/^snowstorm.rest-api.readonly=true/snowstorm.rest-api.readonly=false/' \
  /opt/snowstorm/application-local.properties
sudo systemctl restart snowstorm

Create an import job (SNAPSHOT type), note the returned import id, and upload the RF2 archive to the job. You can do this from the Swagger interface under the Import endpoints, or with curl:

# Create a SNAPSHOT import and capture its id
IMPORT_ID=$(curl -s -X POST http://127.0.0.1/imports \
  -H 'Content-Type: application/json' \
  -d '{"type":"SNAPSHOT","branchPath":"MAIN","createCodeSystemVersion":true}' \
  -i | awk -F/ '/Location:/ {print $NF}' | tr -d '\r')
# Upload the licensed RF2 archive to that import job (this can take 30-60+ minutes)
curl -s -X POST "http://127.0.0.1/imports/${IMPORT_ID}/archive" \
  -H 'Content-Type: multipart/form-data' \
  -F file=@SnomedCT_InternationalRF2_PRODUCTION_<release>.zip

Watch progress with curl -s http://127.0.0.1/imports/${IMPORT_ID}. When the import reports completed, return the server to read-only mode for serving and restart it:

sudo sed -i 's/^snowstorm.rest-api.readonly=false/snowstorm.rest-api.readonly=true/' \
  /opt/snowstorm/application-local.properties
sudo systemctl restart snowstorm

Your licensed SNOMED CT content is now served through the REST and FHIR APIs. Full content imports are memory intensive, so make sure the VM is sized appropriately (see Prerequisites) before you import.

Step 9 - The no-default-credential security model

The image ships with no known credential. The first boot of each VM generates a unique password for Elasticsearch's elastic superuser and writes it to a 0600 root:root file. Elasticsearch is reachable only on the loopback interface, and the terminology API is served read-only, so no unauthenticated caller can create or change content.

A round-trip check proves that the per-VM Elasticsearch password authenticates, that anonymous access and the well-known Elasticsearch defaults are rejected, and that a request that changes content is refused:

sudo bash /usr/local/sbin/snowstorm-cred-roundtrip.sh
OK per-VM elastic auth works; anon+ES-defaults rejected; /version+/codesystems 200; mutating POST rejected (read-only, HTTP 403)

Step 10 - Architecture and where your data lives

nginx on port 80 reverse proxies to Snowstorm on 127.0.0.1:8080, which stores all terminology data in the single-node Elasticsearch 8.11.1 datastore on 127.0.0.1:9200. Elasticsearch holds its indices under /var/lib/elasticsearch on the VM disk, which is where your imported SNOMED CT content lives. The /healthz endpoint returns 200 for load balancer probes.

curl -sI http://127.0.0.1/healthz | head -1 sudo du -sh /var/lib/elasticsearch

To protect your terminology data, snapshot the VM disk or use Elasticsearch's own snapshot and restore against a repository you control.

Using your own domain and TLS

Snowstorm serves plain HTTP on port 80. For production, put your own domain and a TLS-terminating proxy or load balancer in front of the VM. Snowstorm reads X-Forwarded-* headers, which the bundled nginx already sets, so links and the Swagger server URL resolve correctly behind a proxy.

Support

SecureAnyCloud publishes this image and supports it 24/7. Snowstorm is developed by SNOMED International and released under the Apache 2.0 licence. SNOMED International licenses SNOMED CT content separately, and the content is not included in this image. Elasticsearch is included solely as the unmodified backing datastore for Snowstorm.

For product documentation, see the Snowstorm project. For issues with the SecureAnyCloud image, contact SecureAnyCloud support.

Deploy on Azure

Launch Snowstorm SNOMED CT Terminology Server 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