Build recipes

Open-FDD ships two named product recipes. Compose helpers below map onto them.

Named recipe Best for Services Arch Guide
Recipe 1 — cloud hub Railway / cloud MQTTS hub openfdd-mqtt + openfdd-central + openfdd-web linux/amd64 only Railway hub · Railway deployment
Recipe 2 — OT edge LAN/VPN OT host with local DataFusion openfdd-central + openfdd-web + protocol (fieldbus/…) amd64 + arm64 Local stack · VM deployment

Ingest on Recipe 2: protocol scrape → on-box central / Parquet / DataFusion (broker not required). Optional MQTTS publish to a Recipe 1 hub is off by default.

CSV-only dashboard (csv compose) is Recipe 2 without a protocol container. nightly is the floating green-master channel; sha-<7> (newest-by-created) is the preferred reproducible pin.

Do not treat “fieldbus-only on-prem + hub-only in cloud” as the only topology — that is the optional WAN path, not Recipe 2.

Images

Image Role
ghcr.io/bbartling/openfdd-central API + FDD engine (DataFusion rule registry)
ghcr.io/bbartling/openfdd-web React engineering UI (container port 8080; local Compose maps host 3000)
ghcr.io/bbartling/openfdd-fieldbus BACnet/IP poller, publishes over MQTTS
ghcr.io/bbartling/openfdd-bacnet-modbus Split BACnet edge process (bacnet-modbus Docker target)
ghcr.io/bbartling/openfdd-haystack Split outbound Haystack HTTP edge process (haystack Docker target)
ghcr.io/bbartling/openfdd-mqtt Mosquitto broker (MQTTS on 8883)
ghcr.io/bbartling/openfdd-mcp Slim Rust MCP server (talks to central)

All release images are intended to be publicly pullable from GHCR so an IT department, Railway, or a local Docker host does not need a repository credential just to run Open-FDD. Package visibility is a GitHub Package setting and should be checked whenever a new GHCR package name is introduced.

See Release channels and GHCR images.

Local Compose helpers at a glance

Helper Named recipe Compose file Services Use
csv Recipe 2 (no protocol) docker/compose.csv.yml central + web (OPENFDD_MQTT_ENABLED=0) IT dashboard / CSV-package / broker-free edge
standalone Recipe 2 (+ optional local mqtt) docker/compose.standalone.yml mqtt + central + web + fieldbus single OT-connected host
central Recipe 1 shape docker/compose.central.yml mqtt + central + web hub for remote edges
edge optional WAN path docker/compose.edge.yml fieldbus only remote OT publisher → Recipe 1
edge-split optional WAN path docker/compose.edge.split.yml profile bacnet_modbus or haystack (mutually selected) migrating OT edge
ot_local_bacnet_modbus Recipe 2 selected connector docker/compose.ot_local_bacnet_modbus.yml web + central + BACnet/Modbus, MQTT off Astra A01/A02 broker-free OT
ot_local_haystack Recipe 2 selected connector docker/compose.ot_local_haystack.yml web + central + Haystack, MQTT off Astra A01/A02 broker-free OT

The central and csv helpers contain no fieldbus service and never start a BACnet/Modbus/Haystack process. Use edge or standalone when the host is deliberately connected to an OT network. This separation is a deployment boundary, not a runtime capability flag.

The split migration recipe builds separate final targets from the Rust package: bacnet-modbus contains only openfdd-bacnet-modbus, haystack contains only openfdd-haystack, and the default compatibility target contains only the legacy openfdd-fieldbus. The bacnet-modbus service uses host networking for BACnet UDP; the optional haystack service has no host network, binds 0.0.0.0 only inside its container, publishes to a host loopback port, and requires both an API key and an explicit outbound HTTP(S) endpoint at startup. The inactive profile can resolve without an endpoint, but the Haystack process rejects a blank endpoint. Do not replace either entrypoint with a runtime profile flag. The recipe is a source/build checkpoint; no GHCR publication or live OT qualification is claimed here.

Bring a recipe up

openfdd_stack_up.sh pulls the GHCR images for the recipe (unless --no-pull/--build), runs docker compose up -d, and waits on GET /api/health (except the edge recipe):

# Recommended behind-firewall dashboard
export OPENFDD_IMAGE_TAG=nightly
export OPENFDD_JWT_SECRET='replace-with-a-long-random-secret'
export OPENFDD_ADMIN_PASSWORD='replace-with-a-strong-password'
./scripts/openfdd_stack_up.sh csv

# OT-connected alternatives
./scripts/openfdd_stack_up.sh standalone
./scripts/openfdd_stack_up.sh central
OPENFDD_MQTT_HOST=hub.example.com \
OPENFDD_SITE_ID=site-a \
OPENFDD_EDGE_KIT_DIR=./deploy/mqtt/kits/site-a__fieldbus-1 \
  ./scripts/openfdd_stack_up.sh edge

# Developer-only local source build instead of pulling GHCR:
./scripts/openfdd_stack_up.sh csv --build

Pull without starting:

./scripts/openfdd_stack_pull.sh csv   # or standalone|central|edge|mcp|all

After csv boot: UI on http://<host>:3000, API health on http://<host>:8080/api/health.

Recipes in detail

central + web with OPENFDD_MQTT_ENABLED=0. No broker or fieldbus images are required. This is the cleanest recipe for an IT department hosting Open-FDD as a dashboard VM behind its firewall, and it is also the local equivalent of the Railway minimal cloud-lab topology.

./scripts/openfdd_stack_up.sh csv

The Compose recipe persists central state in the repository workspace/ directory. For an IT-managed VM, back up that directory and prefer an immutable sha-* image tag after qualification.

Full VM guide: VM deployment.

standalone — everything on one OT-connected host

mqtt + central + web + fieldbus. The all-on-edge box: BACnet polling, broker, engine, and UI on a single machine. fieldbus runs on the host network for BACnet/IP.

./scripts/openfdd_stack_up.sh standalone

The fieldbus service mounts the named fieldbus-state volume at /edge-state. The opt-in priority history scheduler uses that volume for its durable cursor and bounded P1–P16 snapshots. It is disabled by default. To enable it, provide a trusted field-device catalog and set:

OPENFDD_PRIORITY_SCAN_ENABLED=1 \
OPENFDD_PRIORITY_SCAN_INTERVAL_SECS=3600 \
OPENFDD_PRIORITY_SCAN_MAX_POINTS_PER_DEVICE=100 \
./scripts/openfdd_stack_up.sh standalone

The scheduler visits one configured device after a delay following each completed visit, defaults to one hour, enforces a five-minute minimum, and does not catch up after a restart. It reads only configured points and never performs discovery, writes, priority releases, or remediation. The edge history endpoint is POST /api/connector/priority-history; the authenticated central projection is POST /api/connectors/{edge_id}/priority-history. Operators and admins can request one bounded visit through POST /api/connectors/{edge_id}/priority-history/trigger; viewers are denied before the edge proxy is contacted. The trigger shares the scheduler’s non-overlap guard and does not bypass the explicit enable flag.

History is retained on the edge in an append-only, fsynced JSONL journal with bounded checkpoint compaction. Torn trailing lines are repaired on restart; corrupt non-tail state disables history fail-closed while core BACnet polling continues. Cursors cannot address beyond the retained 100,000-record window; page traversal stops after the requested offset, page, and one continuation probe. The cloud recipes do not create an edge store or scanner.

central — hub for remote edges

mqtt + central + web. Run the hub on a local server or private infrastructure; remote fieldbus edges attach over MQTTS with the edge recipe.

./scripts/openfdd_stack_up.sh central

edge — fieldbus attach

fieldbus only, host networking for BACnet/IP, needs outbound TCP 8883 to a central broker. Required env: OPENFDD_MQTT_HOST, OPENFDD_SITE_ID, OPENFDD_EDGE_KIT_DIR (path to the provisioning kit for this edge).

The edge recipe also mounts fieldbus-state at /edge-state so a restart does not reset the stable device cursor or discard retained history. History is edge-local in this phase. MQTT history synchronization is a separate future contract; the cloud service does not invent HTTP-over-MQTT and does not boot a fieldbus scanner.

OPENFDD_MQTT_HOST=hub.example.com \
OPENFDD_SITE_ID=site-a \
OPENFDD_EDGE_KIT_DIR=./deploy/mqtt/kits/site-a__fieldbus-1 \
  ./scripts/openfdd_stack_up.sh edge

Environment reference

Variable Default Notes
OPENFDD_IMAGE_TAG nightly Channel/tag for every stack image
OPENFDD_*_IMAGE ghcr.io/bbartling/openfdd-*:<tag> Override a single image (e.g. OPENFDD_CENTRAL_IMAGE)
OPENFDD_SITE_ID local Site identifier / MQTT topic namespace
OPENFDD_EDGE_ID fieldbus-1 Edge identifier (fieldbus/edge recipes)
OPENFDD_MQTT_HOST — Broker hostname (required for edge)
OPENFDD_MQTT_ENABLED 1 (0 for csv) Toggle MQTT ingest on central
OPENFDD_EDGE_KIT_DIR — Provisioning kit path (required for edge)
OPENFDD_JWT_SECRET — Enable UI login; pair with OPENFDD_ADMIN_PASSWORD
OPENFDD_ADMIN_PASSWORD — admin/operator/viewer password when JWT is set
OPENFDD_CENTRAL_BIND 0.0.0.0 Central host bind; use 127.0.0.1 on a dashboard VM unless direct API access is needed
OPENFDD_WEB_BIND 0.0.0.0 React web host bind (LAN/VPN access)
OPENFDD_CENTRAL_UPSTREAM central:8080 Runtime web proxy target; Railway uses private service DNS
OPENFDD_PRIORITY_SCAN_ENABLED 0 Explicitly enable the read-only edge priority history scheduler
OPENFDD_PRIORITY_SCAN_INTERVAL_SECS 3600 Delay after each completed device visit; minimum 300
OPENFDD_PRIORITY_SCAN_MAX_POINTS_PER_DEVICE 100 Bound configured points visited per device cycle; maximum 1000
OPENFDD_EDGE_STORE_DIR /edge-state in fieldbus recipes Durable edge store root; do not mount this into central/cloud

Pin a build by SHA across a recipe:

OPENFDD_IMAGE_TAG=sha-abc1234 ./scripts/openfdd_stack_up.sh csv