Deployment topology
Source: docker-compose.yml (repository root). This page documents the containerized stack as declared in compose — process list, ports, images, environment, volumes, and the request path from UI to adapter.
Processes
Four processes carry no application state of their own: durable state lives in Restate, and the run index lives in a separate database (SQLite or Postgres — see Run store).
| Process | Role | Port(s) | Image / build |
|---|---|---|---|
| restate | Durable control plane: journal, replay, run suspension | 8080 (ingress), 9070 (admin) | docker.restate.dev/restatedev/restate:1.4 |
| endpoint | The engine: loads plugins + instance, runs the meridianWorkflow and record Restate services |
9080 | Dockerfile, command pnpm --filter @meridian/api run restate |
| proxy | REST API: events, runs, records, catalog, spec persistence; serves /healthz and /readyz |
4000 | Dockerfile, command pnpm --filter @meridian/api run server |
| ui | Design/execution console (Next.js, config-plane) |
3000 | Dockerfile.ui |
Two additional services back a full local stack:
| Process | Role | Port(s) | Image / build |
|---|---|---|---|
| ehr-lab | Mini-EHR test bench; workflow validations round-trip through FHIR Task, with no direct link to proxy |
3200 | Dockerfile.ehr-lab |
| blaze | FHIR R4 warehouse backing ehr-lab (directory, records, messaging, audit, validations) |
8090→8080 | samply/blaze:1.10.1 |
endpoint and proxy are built from the same Dockerfile and the same workspace; only the command differs. Both run TypeScript sources directly via tsx (no AOT compilation step for the host).
Request flow
UI → proxy → (ingress) restate → endpoint → adapters (FHIR, …)
End-to-end trace shape (see Observability signals):
POST /api/events (proxy)
└─ restate.ingress (proxy)
└─ workflow.run (endpoint)
├─ node <id> (one span per executed node; map body: node <id>#<i>)
│ └─ fhir GET|POST|PUT / posos.graphql (external I/O, under the calling node)
└─ …
endpoint exposes two fixed Restate services (apps/api/src/restate/registry.ts, apps/api/src/restate/services.ts):
| Service | Handlers | Role |
|---|---|---|
meridianWorkflow |
run, resolveValidation, getRun |
A single generic durable workflow that interprets whatever WorkflowSpec is passed in its run input — defining or editing a workflow never adds a Restate service. |
record |
snapshot |
Read-only proxy onto the declared record ports (record: { contextKind } adapters — e.g. patient), exposed over the ingress so proxy can serve GET /api/records/:kind/:id without depending on adapters or the domain directly. |
proxy reaches endpoint's services only through the Restate ingress (RESTATE_URL), never by calling endpoint directly.
Compose services
restate
- Image:
docker.restate.dev/restatedev/restate:1.4. RESTATE_NODE_NAME=restate-dev— pinned so the node identity survives container recreation (default is the container hostname/id; a new id on recreation makes Restate refuse the existingrestate-datavolume withRT0002).- Ports:
8080(ingress),9070(admin — deployment registration). extra_hosts: host.docker.internal:host-gateway— lets the container reach anendpointrunning on the host in dev mode.- Volume:
restate-data:/restate-data(see Run store and Back up and restore for what this volume holds). - Healthcheck: TCP probe on port
9070. - No
profiles:key — started by plaindocker compose up -das well as by--profile stack.
endpoint (profile stack)
- Build:
Dockerfile, context.. - Command:
pnpm --filter @meridian/api run restate. - Volume:
./external-plugins:/app/external-plugins:ro— external plugins are mounted from the host, not baked into the image; build them first withpnpm run build:plugins. In a real deployment they instead come from the plugin registry (manifestplugins:+MERIDIAN_REGISTRY— see Set up a plugin registry). - Environment:
PLUGINS_PATH(points at the three mounted plugindist/dirs),INSTANCE_CONFIG(/app/${INSTANCE_FILE:-instance.dev.yaml}),RESTATE_ADMIN_URL=http://restate:9070,RESTATE_ENDPOINT_URL=http://endpoint:9080,LOG_LEVEL,OTEL_EXPORTER_OTLP_ENDPOINT,ANTHROPIC_API_KEY,OPENAI_API_KEY. depends_on: restatewithcondition: service_healthy.- No published ports in compose (reached only via the Restate ingress).
- On startup,
endpointself-registers againstRESTATE_ADMIN_URL(retried; disable withRESTATE_AUTO_REGISTER=false).
proxy (profile stack)
- Build:
Dockerfile, context.(same image asendpoint). - Command:
pnpm --filter @meridian/api run server. - Port:
4000:4000. - Environment:
PLUGINS_PATH(same three paths asendpoint),RESTATE_URL=http://restate:8080,INSTANCE_CONFIG,RUN_DB_URL(defaultsqlite:/data/runs.sqlite),LOG_LEVEL,OTEL_EXPORTER_OTLP_ENDPOINT,ANTHROPIC_API_KEY,OPENAI_API_KEY. - Volumes:
proxy-data:/data(run index, when SQLite),./external-plugins:/app/external-plugins:ro. depends_on: restate(service_healthy),endpoint(service_started).- Healthcheck:
GET /healthzon port4000.
ui (profile stack)
- Build:
Dockerfile.ui, context.. - Port:
3000:3000. - Environment:
API_URL=http://proxy:4000— resolved server-side at runtime by Next.js rewrites, not baked in at build time. depends_on: proxywithcondition: service_healthy.
ehr-lab (profile stack)
- Build:
Dockerfile.ehr-lab, context.. - Port:
3200:3200. - Environment:
FHIR_BASE_URL(defaulthttp://blaze:8080/fhir),POSOS_API_URL,POSOS_AUDIENCE,GOOGLE_APPLICATION_CREDENTIALS. depends_on: blaze(service_started).- Healthcheck:
GET /api/healthon port3200. - Workflow validations transit through FHIR
Taskresources —ehr-labhas no direct dependency onproxy. To exercise workflows end to end against this same warehouse, pointendpoint/proxyat it too (INSTANCE_FILE=instance.example.yaml, adapterbaseUrl: http://blaze:8080/fhir).
blaze (profile stack)
- Image:
samply/blaze:1.10.1— pinned (not a moving tag: a version drift once hid a FHIR-search compatibility bug against the deployed warehouse). - Environment:
STORAGE=standalone(persistent RocksDB, not in-memory),BASE_URL=http://blaze:8080,JAVA_TOOL_OPTIONS=-Xmx2g. - Port:
8090:8080. - Volume:
blaze-data:/app/data. - Sole datastore behind
ehr-lab: directory, records, messaging, audit, and validation tasks.
lgtm (profile obs)
- Image:
grafana/otel-lgtm:0.8.1— Tempo + Prometheus + Loki + Grafana, pre-wired, in one container. - Ports:
3001:3000(Grafana UI, remapped to avoid clashing withui),4317:4317(OTLP gRPC),4318:4318(OTLP HTTP — used by the app's-httpexporters). - Volume:
lgtm-data:/data. - Healthcheck:
GET /api/healthon port3000(in-container). - See Enable observability and Observability signals.
postgres (profile pg)
- Image:
postgres:17-alpine. - Environment:
POSTGRES_USER=clinical,POSTGRES_PASSWORD(defaultclinical),POSTGRES_DB=clinical. - Port:
5432:5432. - Volume:
pg-data:/var/lib/postgresql/data. - Healthcheck:
pg_isready -U clinical. - Backs
RUN_DB_URL=postgres://clinical:clinical@postgres:5432/clinical— see Use the Postgres run index.
Volumes
| Volume | Mounted by | Contents |
|---|---|---|
restate-data |
restate |
Durable execution state: journal, run status, suspensions. |
proxy-data |
proxy |
SQLite run index (RUN_DB_URL=sqlite:/data/runs.sqlite, default). |
blaze-data |
blaze |
FHIR warehouse storage (RocksDB, standalone mode). |
lgtm-data |
lgtm |
Tempo/Prometheus/Loki data for the local observability stack. |
pg-data |
postgres |
Postgres data directory, when the pg profile backs the run index. |
Profiles
| Profile | Services | Purpose |
|---|---|---|
| (none) | restate |
Dev default: docker compose up -d starts only Restate; endpoint/proxy/ui run on the host via pnpm run dev (or dev:fhir). |
stack |
restate, endpoint, proxy, ui, ehr-lab, blaze |
Full containerized stack, images built from this repository. |
pg |
postgres |
Adds a Postgres-backed run index; combine with stack and set RUN_DB_URL. |
obs |
lgtm |
Adds the local Tempo/Prometheus/Loki/Grafana stack; combine with stack and set OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318. |
Related documents
- Create your first instance — end-to-end setup of a running instance.
- Environment variables — full variable reference per process.
- Run store — run index schema and SQLite/Postgres semantics.
- Observability signals — logs, traces, metrics, and probes in detail.
- Enable observability — wiring the
obsprofile. - Use the Postgres run index — wiring the
pgprofile. - Back up and restore — what to snapshot per volume.
- Upgrade the platform — restart order across these processes.
- Sizing — scaling guidance for this topology.
- Security posture — which ports must stay unpublished.