How to Run a Self-Hosted FHIR R4 Server Locally with Ilavrita
How to Run a Self-Hosted FHIR R4 Server Locally with Ilavrita
Primary query: self-hosted FHIR server local
Version: dev
Source revision: 7e036d413940d3978a5042409d387a4d52f2485c
Last verified: September 29, 2026
FHIR version: R4 4.0.1
Data: synthetic only
A local FHIR server should be boring to start and precise about what “working” means.
For Ilavrita, the shortest path is four commands:
git clone https://github.com/Ilavrita/Ilavrita.git
cd Ilavrita
make bootstrap
make run
Then:
curl -s http://127.0.0.1:8090/healthz
curl -s http://127.0.0.1:8090/version
curl -s -H 'Accept: application/fhir+json' http://127.0.0.1:8090/fhir/R4/metadata
If those endpoints answer, you have an Ilavrita process serving its FHIR R4 surface locally.
That does not mean you have a production-ready clinical system. It does not mean the build is appropriate for patient data. It does not make the deployment HIPAA-, GDPR-, EHDS-, or otherwise compliant. It means a specific development build started and exposed the interfaces its current tests say it exposes.
That distinction is the point of this guide.
Before you clone: what the current build expects
At the current dev revision, the repository declares:
- Go 1.27
- Node 22+ and pnpm 10+ for workspace tooling
- a pinned Ilavrita PocketBase fork as a Git submodule
- FHIR R4 4.0.1
The server itself is Go. The workspace also contains TypeScript tooling, API documentation tooling, and other packages, which is why the README lists Node and pnpm as development requirements.
There is one non-obvious dependency: Ilavrita does not compile against a PocketBase version fetched normally from the Go module proxy. go.mod replaces the PocketBase module with:
./third_party/pocketbase
That directory is a pinned Git submodule.
A plain source checkout without that submodule is therefore intentionally incomplete.
Source: go.mod at this revision.
Step 1: clone the repository
For ordinary development:
git clone https://github.com/Ilavrita/Ilavrita.git
cd Ilavrita
If you are reproducing this article rather than following whatever dev becomes next week, pin the same revision:
git checkout 7e036d413940d3978a5042409d387a4d52f2485c
Pinning matters because Ilavrita's capability surface is moving. A tutorial without a revision eventually becomes a description of no real build at all.
You can confirm the checkout with:
git rev-parse HEAD
Expected for this article:
7e036d413940d3978a5042409d387a4d52f2485c
Step 2: run make bootstrap
Now:
make bootstrap
The target is deliberately small. It calls scripts/bootstrap.sh, which does three things:
git submodule update --init --recursive
go mod download
pnpm install # when pnpm is available
That first line is the important one for the server build. It fetches the pinned PocketBase fork into third_party/pocketbase.
The script is fail-fast: if the submodule or Go dependency setup fails, it stops rather than leaving a checkout that looks ready.
If pnpm is not installed, the script explicitly skips workspace dependency installation. That can be enough for a Go-only server run, but it is not enough for the full monorepo development workflow. For the repository as a whole, follow the documented Node 22+ / pnpm 10+ requirements.
Source: scripts/bootstrap.sh.
Step 3: choose fast startup or reproducible build identity
The README's fastest path is:
make run
That executes:
go run ./apps/ilavrita serve
and starts the development server on the local PocketBase runtime.
There is a small but important consequence.
The source defines:
version = "dev"
revision = "unknown"
Those values are replaced with Git-derived build metadata by the Makefile's build target. The run target does not pass those linker flags.
So a source run may answer:
{
"version": "dev",
"revision": "unknown",
"fhirVersion": "4.0.1"
}
That is not the server forgetting which Git commit exists. It is the executable never having been stamped with it.
If reproducible build identity matters for what you are testing, use:
make build
./bin/ilavrita serve
make build injects both:
VERSION = git describe --tags --always --dirty
REVISION = git rev-parse HEAD
into the binary with Go linker flags.
For evidence-heavy testing, that is the better path. A running process that can tell you its source revision is easier to reason about than one whose provenance lives only in your terminal history.
Source: Makefile and apps/ilavrita/main.go.
Step 4: prove the process is alive
Start with the boring endpoint:
curl -s http://127.0.0.1:8090/healthz
The documented healthy response is:
{"status":"ok"}
This proves very little, which is why it is useful.
A liveness endpoint should answer one narrow question: is this process up and serving requests?
It does not prove FHIR behavior, authentication, authorization, persistence, conformance, or production readiness.
Next:
curl -s http://127.0.0.1:8090/version
This identifies the running software and FHIR version. If you used make build, this is also where you should expect the source revision to line up with your checkout.
The OpenAPI description treats /version explicitly as build identity: the route exists so a running instance can be matched to source and release artifacts.
Step 5: ask the FHIR server what it claims to support
Do not infer the server's FHIR surface from this article.
Ask the server.
curl -s -H 'Accept: application/fhir+json' http://127.0.0.1:8090/fhir/R4/metadata
The response is a FHIR R4 CapabilityStatement.
That is the standard FHIR mechanism for discovering what a server says it supports. In Ilavrita, the statement is generated from the routes the running server registered rather than maintained as an unrelated static document.
At the verified revision, the README describes 126 declared R4 resource types. But the useful rule is not “memorize 126.”
The useful rule is: trust the running CapabilityStatement for the build you are actually talking to.
Ilavrita's API documentation makes the same distinction. OpenAPI describes the broader HTTP surface for tooling and human reference; GET /fhir/R4/metadata describes the FHIR behavior of the deployment answering the request.
FHIR reference: HL7 R4 RESTful API — Capability Statement.
Ilavrita reference: docs/api.md.
A 401 on a FHIR resource is a successful smoke test
Now try:
curl -s -i http://127.0.0.1:8090/fhir/R4/Patient/123
Without an authenticated principal, the expected result is not a Patient.
It is:
HTTP/1.1 401 Unauthorized
Content-Type: application/fhir+json
with an OperationOutcome body.
That is useful evidence.
It shows you reached Ilavrita's FHIR boundary, the route exists, and the authentication requirement was enforced. A tutorial that treats every non-200 response as failure misses one of the most important things a healthcare API is supposed to do.
The README is explicit: every FHIR route requires an authenticated principal.
The metadata endpoint is different because clients need to discover the server's FHIR capabilities before they necessarily have a user session.
A 404 and a 501 mean different things
Ilavrita intentionally distinguishes a resource type it does not declare from an interaction it does not implement.
The README uses two useful probes:
curl -s -i http://127.0.0.1:8090/fhir/R4/Appointment/123
At the verified revision, Appointment is not one of the declared resource endpoints, so the documented response is 404 Not Found.
Now:
curl -s -i http://127.0.0.1:8090/fhir/R4/Patient/_history
Type-level history is not implemented, so the documented response is 501 Not Implemented.
Both are returned as FHIR OperationOutcome responses rather than raw framework errors.
This is a more useful local smoke test than “the port opened.” It checks the shape of the server's refusal boundary too.
The container path is shorter, but it proves something different
If you prefer Docker:
docker compose up --build
The repository's compose.yaml builds the current checkout, publishes port 8090, and keeps server data in a named volume.
There is also a release-image pattern documented in the README:
docker run --rm -p 8090:8090 -v ilavrita-data:/data ghcr.io/ilavrita/ilavrita:latest
For a reproducible engineering investigation, latest is usually the wrong tag to write into your notes. Use a release-specific image/digest when you need to know exactly which artifact was tested.
The compose file also carries an operational warning that should not be skipped:
terminate TLS in front of this before exposing it.
Local development over loopback and internet-facing deployment are different problems.
The first authenticated workflow is deliberately not “sign up”
A fresh Ilavrita install begins with no ordinary user who can simply register themselves.
Startup provisions an install claim token for whoever controls the host. The bootstrap tests hold several properties around that path: the token is handed over through the data directory, it is not meant to be emitted casually into logs, a wrong claim writes nothing, and a successful claim is single-use.
That design prevents “first person to reach the public port becomes administrator.”
This guide intentionally stops before walking through the full Project/identity/AccessPolicy setup because the primary goal here is to verify a local FHIR server surface, not to turn a smoke test into an authorization tutorial.
The tests for the first-install boundary are in apps/ilavrita/bootstrap_test.go.
What I could and could not reproduce for this post
I attempted the clean-clone path in a fresh runtime directory:
git clone --recurse-submodules https://github.com/Ilavrita/Ilavrita.git /tmp/ilavrita-blog-verify
The environment failed before Git could fetch the repository:
fatal: unable to access 'https://github.com/Ilavrita/Ilavrita.git/':
Could not resolve host: github.com
So I did not run make bootstrap, compile the server, or execute the curl probes in that runtime.
The current dev branch, Makefile, bootstrap script, README, roadmap, security documentation, known-limitations document, API documentation, and bootstrap tests were instead inspected through the connected GitHub repository at revision 7e036d413940d3978a5042409d387a4d52f2485c.
That is source verification, not a successful clean-clone execution.
There is also documentation sync lag right now
The Payload-synced documentation metadata still identifies revision:
946221398bc8fa8731838e98e3d040c102df7b63
with a September 20 source date.
GitHub dev is now:
7e036d413940d3978a5042409d387a4d52f2485c
For this post, current GitHub dev therefore wins when the two disagree.
There is also stale prose inside the repository itself. For example, introductory parts of the current README/security/known-limitations corpus still contain older statements about profile validation even though later docs and tests show partial declared-profile validation is implemented.
None of those stale statements are needed for this quickstart, so they are not repeated here.
The broader lesson is the same one behind /version: if documentation can drift, pin the code you actually ran.
What a successful local run proves
If you can complete this guide on your machine, you have useful evidence:
the server starts, operational endpoints respond, FHIR R4 metadata is available, an unauthenticated FHIR resource request is refused, and the running CapabilityStatement tells you what that particular build advertises.
What you do not have is evidence that the build belongs on a public clinical workload.
The current repository still says not to put patient data in this build. It has not had an external security review. It makes no HIPAA, GDPR, EHDS, or certification claim. Several FHIR and operational capabilities remain deliberately incomplete.
That is not a footnote to the quickstart.
It is part of the quickstart.
The fastest way to lose confidence in infrastructure is to make “it booted on my laptop” carry more meaning than it deserves.
The five-minute verification checklist
After the server starts, the sequence I would keep in a terminal history is:
git rev-parse HEAD
curl -s http://127.0.0.1:8090/healthz
curl -s http://127.0.0.1:8090/version
curl -s -H 'Accept: application/fhir+json' http://127.0.0.1:8090/fhir/R4/metadata
curl -s -i http://127.0.0.1:8090/fhir/R4/Patient/123
The first line says what source you intended to run.
The second says the process is alive.
The third says what the process says it is.
The fourth says what FHIR surface it claims.
The fifth confirms that a FHIR resource is not anonymously handed back just because the server is local.
That is a much better definition of “running locally” than a screenshot of a terminal saying the port opened.
For the current product boundary, see the Ilavrita docs, known limitations, and the roadmap.
Source references for this article:
