Snowstorm SNOMED CT Terminology Server on Ubuntu 24.04 on Azure User Guide
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, supporting 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. It underpins clinical systems, terminology browsers and data pipelines that need an authoritative, queryable SNOMED CT service.
The cloudimg image ships the full server stack wired together and ready on the first boot of every VM: Snowstorm 10.11.2 (Apache 2.0) running on Eclipse Temurin 17, backed by a single node Elasticsearch 8.11.1 datastore (the exact Elasticsearch version Snowstorm is tested against), all as systemd services on a hardened, fully patched Ubuntu 24.04 LTS base. An nginx reverse proxy serves the interactive Swagger interface and the API on port 80. Elasticsearch is bound to the loopback interface only and secured with a per VM password generated on first boot, and the terminology API is served read only so no unauthenticated caller can alter content. The full SNOMED CT content is never bundled, because it requires your own SNOMED CT licence: the server ships healthy and empty, and you import your licensed release when you are ready. Backed by 24/7 cloudimg support.
What is included:
- Snowstorm 10.11.2 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.1only - Eclipse Temurin 17 as the Java runtime
- A per VM Elasticsearch superuser password generated on first boot and recorded in a root only file; no shipped default credential
- Read only API by default, so mutating and administrative operations are protected from unauthenticated callers
- Java heaps tuned for the recommended 8 GiB VM (Elasticsearch 3 GiB, Snowstorm 2 GiB)
elasticsearch.service,snowstorm.service,nginx.serviceandsnowstorm-firstboot.serviceas systemd units- An unauthenticated
/healthzendpoint for Azure Load Balancer health probes - 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 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 (for example to a memory optimised Standard_E4s_v5 with 32 GiB) before importing a full release. NSG inbound: allow 22/tcp from your management network and 80/tcp for the API. Snowstorm serves plain HTTP on port 80; 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 will also need your own SNOMED CT licence and RF2 release files to load real content. SNOMED CT is licensed by SNOMED International; in many countries it is available at no charge through a national release centre. See https://www.snomed.org/get-snomed. The cloudimg image deliberately 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 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). Then Review + create and Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name snowstorm \
--image <marketplace-image-urn> \
--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 that runs once on the first boot to mint 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, and the Java runtime is Eclipse Temurin 17.

Step 4 - Retrieve the per VM credentials
A unique Elasticsearch superuser password is generated on the first boot of every VM and written 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 and lets you call every endpoint of the terminology server. Because the image ships in read only mode, the content changing and administrative operations are disabled and hidden, so 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, which is the expected empty but healthy state 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 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, providing the CodeSystem, ValueSet and ConceptMap resources and operations such as $lookup, $expand, $validate-code, $subsumes and $translate. After you import a release these back onto 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 you provide your own licensed SNOMED CT RF2 release and run a one time import. Snowstorm's recommended flow is to import while writable, then return the server 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:
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) and note the returned import id, then upload the RF2 archive to it. 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 it reports completed, return the server to read only mode for serving and restart:
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 importing.
Step 9 - The no default credential security model
The image ships with no known credential. Elasticsearch's elastic superuser password is generated uniquely on the first boot of each VM and written to a 0600 root:root file, and Elasticsearch is reachable only on the loopback interface. The terminology API is served read only, so no unauthenticated caller can create or change content. A round trip check proves the per VM Elasticsearch password authenticates while anonymous access and the well known Elasticsearch defaults are rejected, and that a content changing request 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; that 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
This image is published and supported by cloudimg with 24/7 support. Snowstorm is developed by SNOMED International and released under the Apache 2.0 licence. SNOMED CT content is licensed separately by SNOMED International and 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 cloudimg image, contact cloudimg support.