Skip to content

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 - sct does 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

  1. DNS - point fhir.example.org at your server. Do this first; Let's Encrypt needs it live before Caddy can request a certificate.
  2. ssh in, 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
  1. Bring the stack up:
docker compose up -d --build
  1. 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 DOMAIN set: requests a certificate via Let's Encrypt and starts serving HTTPS, redirecting HTTP to HTTPS;
  • with DOMAIN unset: 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/$lookup
  • CodeSystem/$validate-code
  • CodeSystem/$subsumes
  • ValueSet/$expand
  • ValueSet/$validate-code
  • ConceptMap/$translate when the database has crossmaps loaded

For the full operation reference, see sct serve. For the deployment design and rationale, see spec/deployment.md.