Skip to main content

Local Test Stack

The repository ships a docker-compose.yml at its root that spins up a complete set of protocol servers and simulators so you can develop and test OIBus connectors without access to real industrial equipment. This page documents every service, what it simulates, how to configure it, and how to bring it up.

Quick Start

The easiest way to start the stack is via the npm scripts defined in backend/package.json. Run them from the backend/ directory:

# IoT protocol servers only (OPC UA, Modbus, MQTT)
npm run docker:iot

# IoT servers + simulator (recommended for connector development)
npm run docker:simulator

# PostgreSQL only
npm run docker:database

# FTP / SFTP servers only
npm run docker:ftp

# Full development stack: IoT + simulator + database
npm run docker:dev

# Everything including OIBus runtime and nginx
npm run docker:all

# Tear down all containers
npm run docker:down

You can also invoke Docker Compose directly if you need a custom combination of profiles:

docker compose --profile iot --profile simulator up -d

Services are grouped into Docker Compose profiles:

ProfileServices
iotopcua-server, modbus-server, mqtt-broker
simulatorsimulator
databasepostgres
ftpftp-server, sftp-server
oibusoibus, nginx
Profile independence

The simulator profile requires the iot profile services to be running (Modbus server and MQTT broker). Always start both profiles together: --profile iot --profile simulator (or use npm run docker:simulator which does this automatically).

All services share the internal bridge network oibus-network. Ports are forwarded to localhost so OIBus running outside Docker (i.e. npm start in the backend/ directory) can reach them directly.


Services

OPC UA Server — opcua-server

PropertyValue
Imagemcr.microsoft.com/iotedge/opc-plc
Port50000 (OPC UA TCP)
Configdocker/opcua/nodes_config.json

Microsoft's OPC PLC simulator. It exposes a standard OPC UA server with custom nodes defined in nodes_config.json as well as a set of built-in nodes (boiler simulation, fast/slow changing variables, etc.).

Custom nodes (folder OIBus, all with Historizing: true):

Node IDDescriptionData typeSimulationParameters
1023Temperature (°C)DoubleRandom Walk18 – 28 °C, step 0.5, every 2 s
1024Pressure (hPa)DoubleSine Wave1013.25 ± 10 hPa, period 10 s
1025Flow rate (L/min)DoubleRandom Walk40 – 60 L/min, step 1, every 3 s
1026Humidity (%)DoubleSine Wave65 ± 15 %, period 15 s
1027RPMInt32Random Walk1 200 – 1 800, step 50, every 2.5 s
1028Pump statusBooleanSquare Waveperiod 20 s
1029Voltage (V)DoubleRandom Walk210 – 230 V, step 0.5, every 2 s
1030Current (A)DoubleSine Wave15.2 ± 2 A, period 12 s

Node IDs follow the OPC UA namespace ns=2;i=<NodeId>. The OPC UA address of temperature, for example, is ns=2;i=1023.

Historian support: Historizing: true enables OPC UA Historical Data Access (HA) on every custom node. The server answers HistoryRead requests, making it suitable to test OIBus history-query mode.

In-memory history only

History is stored in RAM — it is not persisted to disk. All historical data is lost when the container restarts. Scenarios that require catch-up after a long gap (days/weeks) cannot be reproduced with this simulator.

Authentication: anonymous access is disabled. Use the credentials configured via the environment variables OPCUA_DEFAULT_PASSWORD (default pass) and OPCUA_ADMIN_PASSWORD (default pass), with the usernames oibus and admin respectively (set in docker-compose.yml).


Modbus Server — modbus-server

PropertyValue
Imageoitc/modbus-server
Port5020 (Modbus TCP)
Configdocker/modbus/server_config.json

A lightweight Modbus TCP server. Its register map is declared in server_config.json. The server accepts writes from any Modbus TCP client, so the Simulator can dynamically update holding registers and coils in real time.

server_config.json key numbering vs OIBus Address offset

The configuration file uses 1-based register keys ("1", "2", …) because the server is configured with "zeroMode": false. This is a detail of the server's config file format only — Modbus TCP at the wire level is always 0-based, so the mapping is simply config key = protocol address + 1.

This is independent of the Address offset setting in OIBus (Modbus vs JBus). When connecting OIBus to this server, keep the default Modbus offset (no offset): OIBus sends 0-based protocol addresses, and the server resolves them against its 1-based keys internally. The JBus offset would only be needed for devices that expose 1-based addresses at the Modbus protocol level itself.

Initial register values (overwritten by the simulator after it connects):

Register typeProtocol addressInitial valueDescription
Input Register0314Firmware version (uint16)
Input Register122136Serial number — low word
Input Register24660Serial number — high word
Discrete Input0truePanel door closed
Discrete Input1trueSafety relay OK
Discrete Input2falseNetwork connected
Discrete Input3falseE-stop pressed

Input registers and discrete inputs are read-only from a Modbus client's perspective, so their values are static and come from server_config.json. Holding registers and coils are updated every 2 seconds by the simulator.


MQTT Broker — mqtt-broker

PropertyValue
Imageeclipse-mosquitto
Ports1883 (MQTT), 9001 (WebSocket)
Configdocker/mosquitto/config/

Eclipse Mosquitto with a custom entrypoint (docker/mosquitto/entrypoint.sh) that injects the MQTT_USER / MQTT_PASSWORD credentials at startup. Anonymous access is disabled.

The 9001 WebSocket port is available for browser-based MQTT clients if needed.


PostgreSQL — postgres

PropertyValue
Imagepostgres
Port5432

A vanilla PostgreSQL instance for testing the South-PostgreSQL connector. Credentials are:

VariableDefault
POSTGRES_USERoibus
POSTGRES_PASSWORDpass
POSTGRES_DBoibus-db

Override passwords via the .env file or shell environment (e.g. POSTGRES_PASSWORD=secret docker compose up).


FTP Server — ftp-server (profile: ftp)

PropertyValue
Imagefauria/vsftpd
Ports20, 21, 21100–21110 (passive)

Passive-mode vsftpd. Credentials: oibus / oibuspass. Files land in docker/ftp/data/.


SFTP Server — sftp-server (profile: ftp)

PropertyValue
Imageatmoz/sftp
Port2222 (SSH)

Single-user SFTP server. Credentials: oibus / pass. Upload directory: docker/sftp/data/.


OIBus Runtime — oibus (profile: oibus)

PropertyValue
Imageghcr.io/optimistiksas/oibus
Port2223 (web UI / API)
Data./data-folder/app/OIBus/OIBusData

The OIBus runtime itself, useful when you want to test the full stack inside Docker rather than running the backend with npm start. See Docker Image for details about this image.


Nginx — nginx (profile: oibus)

PropertyValue
Imagenginx
Ports80 (HTTP), 443 (HTTPS)
Configdocker/nginx/

Reverse proxy in front of the OIBus container. Requires the DOMAIN environment variable and TLS certificates in docker/nginx/certs/. Only needed when testing the full TLS / reverse-proxy setup.


Unified Simulator — simulator

PropertyValue
Imagepython:3.14-slim
Scriptdocker/simulator/simulator.py
Librariespymodbus==3.6.9, paho-mqtt

A single Python script that drives both the Modbus server and the MQTT broker. It runs two daemon threads — one per protocol — each with its own independent retry loop so a failure in one source does not affect the other.

Modbus thread

Writes to the Modbus server every MODBUS_UPDATE_INTERVAL seconds (default 2 s). All values are sinusoidal with 5 % random noise unless stated otherwise.

Holding registers — uint16 (1 word):

Protocol addrNameBaseAmplitudePeriod
0temperature2505060 s
1humidity600200120 s
2pressure10030180 s
3vibration25020030 s
4co2600200300 s
5flow_rate1508090 s

Holding registers — extended data types (multi-word):

Protocol addrNameData typeBaseAmplitudePeriod
6outdoor_tempint16525240 s
7 – 8production_countuint3250 00040 000600 s
9 – 10power_kwfloat75.545.0180 s
11 – 12energy_balanceint3205 000360 s
13 – 16shaft_speeddouble1 500.0300.0120 s
17status_flagsbitfield

status_flags is a 16-bit register whose individual bits are independent square waves:

BitNamePeriod
0motor_running60 s
1fault_detected300 s
2maintenance_due600 s
3overload120 s

Coils (square wave, 1 = on for first half of period):

Protocol addrNamePeriod
0pump_running30 s
1valve_open45 s
2alarm_active120 s
3machine_on20 s
Multi-word encoding

OIBus applies an unconditional swap32() + swap16() on multi-word values before reading them. The simulator accounts for this by writing the low 16-bit word before the high 16-bit word within each 32-bit dword. This matches the default OIBus settings (swapWordsInDWords: false, endianness: big-endian).

MQTT thread

Publishes to the MQTT broker every MQTT_UPDATE_INTERVAL seconds (default 2 s). Topics follow the pattern <workshop>/<sensor>/<type>. All values are sinusoidal with 5 % random noise.

TopicBaseAmplitudePeriod
workshop1/sensor1/temperature30.010.060 s
workshop1/sensor2/humidity55.025.0120 s
workshop1/sensor3/pressure1 000.050.0180 s
workshop1/sensor4/vibration5.05.030 s
workshop2/sensor1/temperature28.08.090 s
workshop2/sensor2/humidity50.020.0150 s
workshop2/sensor3/pressure990.040.0210 s
workshop2/sensor4/vibration4.04.045 s

Environment variables

VariableDefaultDescription
RETRY_INTERVAL10Seconds between reconnection attempts
MODBUS_HOSTmodbus-serverHostname of the Modbus server
MODBUS_PORT5020Modbus TCP port
MODBUS_SLAVE_ID1Modbus slave / unit ID
MODBUS_UPDATE_INTERVAL2Seconds between Modbus write cycles
MQTT_BROKERmqtt-brokerHostname of the MQTT broker
MQTT_PORT1883MQTT port
MQTT_USERoibusMQTT username
MQTT_PASSWORDpassMQTT password (also set via $MQTT_PASSWORD)
MQTT_UPDATE_INTERVAL2Seconds between MQTT publish cycles

Passwords and Secrets

Sensitive values are read from environment variables and default to pass if not set. Create a .env file at the repository root to override them locally without touching docker-compose.yml:

.env
MQTT_PASSWORD=my_mqtt_secret
POSTGRES_PASSWORD=my_pg_secret
OPCUA_DEFAULT_PASSWORD=my_opcua_secret
OPCUA_ADMIN_PASSWORD=my_admin_secret
DOMAIN=oibus.example.com

.env is listed in .gitignore — it will never be committed.


Useful Commands

# Start the recommended development stack (IoT servers + simulator + database)
npm run docker:dev

# Tail simulator logs (Modbus + MQTT writes)
docker compose logs -f simulator

# Restart the simulator after changing docker/simulator/simulator.py
docker compose --profile simulator up -d --force-recreate simulator

# Restart the Modbus server after changing docker/modbus/server_config.json
docker compose --profile iot up -d --force-recreate modbus-server

# Stop everything and remove containers (data volumes are kept)
npm run docker:down