Build From Source
Run a FHIR R4 SNOMED CT terminology server on a clean VPS with Docker Compose - HTTPS included. This route builds the sct image from a git clone, so it's the one to pick if you want to patch the code, pin a specific commit, or build for a platform the published image doesn't cover. If you just want the server running, Docker Image is faster - no clone, no build.
The stack is two containers: sct (builds the database on first boot and serves FHIR), and Caddy in front of it (automatic TLS, optional basic auth, CORS). Caddy owns the public ports; sct is never reachable directly.
Prerequisites
- Docker with the Compose plugin.
- An NHS TRUD account subscribed to the SNOMED CT UK Monolith Edition, and your TRUD API key. It is used once, to download the release under your own licence -
sctdoes not redistribute SNOMED CT content, and the running server never contacts TRUD again once the database is built. - For real HTTPS: a domain name with its DNS already pointing at this server, and ports 80 + 443 reachable from the internet (Let's Encrypt needs both to issue a certificate).
The four-step self-host
- DNS - point
fhir.example.orgat your server. Do this first; Let's Encrypt needs it live before Caddy can request a certificate. sshin, clone, and configure:
git clone https://github.com/pacharanero/sct.git
cd sct
cp .env.example .env
$EDITOR .env
Set at minimum:
TRUD_API_KEY=your-trud-api-key
DOMAIN=fhir.example.org
ACME_EMAIL=you@example.org
- Bring the stack up:
docker compose up -d --build
curl https://fhir.example.org/fhir/metadata- works once the first-run build and certificate issuance complete (see What first boot does).
Fast path (local / no domain)
To try it locally without DNS or a real certificate, just skip DOMAIN:
git clone https://github.com/pacharanero/sct.git
cd sct
TRUD_API_KEY=your-key-here docker compose up -d --build
Caddy serves plain HTTP on port 80 (no DOMAIN means no automatic HTTPS - there's no hostname to get a certificate for):
curl http://localhost/fhir/metadata
What first boot does
If the sct-data volume doesn't already contain a database, the entrypoint runs:
sct trud download --edition uk_monolith --skip-if-current --pipeline --refsets all --locale en-GB
That downloads the release, builds a SNOMED SQLite database (a few minutes for the UK Monolith), and starts sct serve on an internal port. Meanwhile Caddy:
- with
DOMAINset: requests a certificate via Let's Encrypt and starts serving HTTPS, redirecting HTTP to HTTPS; - with
DOMAINunset: serves plain HTTP immediately.
Either way, while sct is still building, Caddy returns a 503 with a clear message ("sct is starting up (downloading/building the SNOMED database) - retry shortly.") instead of a bare connection error - so a curl during the first few minutes is expected and self-explanatory, not a sign anything is broken. It resolves to 200 as soon as the build finishes.
Subsequent starts reuse the existing database in the sct-data volume, so they skip the TRUD download and build step entirely - only the very first boot is slow.
Check it works
curl 'https://fhir.example.org/fhir/metadata'
Look up a concept:
curl 'https://fhir.example.org/fhir/CodeSystem/$lookup?system=http://snomed.info/sct&code=22298006'
Expand an ECL ValueSet:
curl 'https://fhir.example.org/fhir/ValueSet/$expand?url=http://snomed.info/sct?fhir_vs=ecl/%3C%3C73211009&count=10'
(Substitute http://localhost for the fast-path / no-domain case.)
Configuration
Edit .env - see .env.example for the full annotated list. The essentials:
| Variable | Default | Description |
|---|---|---|
TRUD_API_KEY |
- | Required for first boot, unless you provide an existing database. Build-time only - the running server never uses it again. |
SCT_TRUD_EDITION |
uk_monolith |
Built-in TRUD edition to download. |
SCT_REFSETS |
all |
all enables ICD-10 / OPCS-4 maps and concept history. |
SCT_LOCALE |
en-GB |
Preferred-term locale. |
SCT_INCLUDE_INACTIVE |
false |
Set true to retain inactive concepts. |
DOMAIN |
(unset) | Your public hostname. Set it for automatic HTTPS; leave unset for plain HTTP on :80. |
ACME_EMAIL |
(unset) | Let's Encrypt account email (cert-expiry notices). Optional but recommended when DOMAIN is set. |
BASIC_AUTH_USER / BASIC_AUTH_HASH |
(unset) | Optional HTTP basic auth - see below. Both must be set together. |
CORS_ORIGINS |
* |
Allowed CORS origins for browser-based FHIR clients. |
Optional basic auth
Terminology data is non-PHI and read-only, so authentication is opt-in - mainly abuse control on a publicly reachable endpoint. Generate a password hash with Caddy's own tool (no need to install Caddy locally - this runs it in a throwaway container):
docker run --rm caddy:2-alpine caddy hash-password --plaintext 'your-password'
Set both in .env:
BASIC_AUTH_USER=yourusername
BASIC_AUTH_HASH=$2a$14$...output from the command above...
Leave both unset for no auth (the default). Setting only one has no effect - both are required together.
Operations
Stop the server:
docker compose down
Upgrade after pulling newer code:
git pull
docker compose up -d --build
Force a fresh download/build by removing the volumes (this also discards the issued TLS certificate, which Caddy will simply re-request):
docker compose down -v
docker compose up -d --build
The sct-data volume contains licensed SNOMED CT data. Do not publish it as part of an image or commit exported database files to git.
FHIR surface
The Docker setup runs the same server as sct serve, including:
CodeSystem/$lookupCodeSystem/$validate-codeCodeSystem/$subsumesValueSet/$expandValueSet/$validate-codeConceptMap/$translatewhen the database has crossmaps loaded
For the full operation reference, see sct serve. For the deployment design and rationale, see spec/deployment.md.