One-command edge deploy
./scripts/deploy.sh pi@edge-01 config.yamlThe script builds the UI first (it is embedded in the binary), cross-compiles with
cargo zigbuild (TARGET_TRIPLE=x86_64-unknown-linux-musl for x86 hosts), creates the
opexmx service user, verifies the uploaded binary’s SHA-256, swaps it in atomically,
keeps the previous one as opex-flow.prev, and rolls back automatically if the service
fails to start.
systemd (manual)
Create the user and directories, put secrets in a root-only env file, and install the
unit from deploy/opex-flow.service:
sudo useradd --system --home-dir /var/lib/opex-flow --shell /usr/sbin/nologin opexmx
sudo install -d /opt/opex-flow /etc/opex-flow
sudo install -m 0755 opex-flow /opt/opex-flow/opex-flow
sudo install -m 0640 -g opexmx config.yaml /etc/opex-flow/config.yaml
sudo install -m 0600 /dev/null /etc/opex-flow/env
sudoedit /etc/opex-flow/env # CONNECTOR_HMAC_SECRET=... CONNECTOR_WEBHOOK_TOKEN=...
sudo cp deploy/opex-flow.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now opex-flowSet connector.data_dir: /var/lib/opex-flow in the config. Key unit settings:
| Setting | Why |
|---|---|
EnvironmentFile=-/etc/opex-flow/env |
Secrets stay out of the world-readable unit. |
StateDirectory=opex-flow |
Writable, service-owned home for data.db, push_queue.db, api_token, license cache and runtime files. |
ProtectSystem=strict, ProtectHome, PrivateTmp, NoNewPrivileges |
Sandbox: only the state directory is writable. |
SupplementaryGroups=dialout |
Modbus RTU on /dev/ttyUSB*. |
TimeoutStopSec=45 |
The connector handles SIGTERM and drains for up to 30 s (adapters stopped, push queue / sinks / batches flushed). |
StartLimitIntervalSec=300, StartLimitBurst=5 |
A bad config or license cannot restart-loop forever. |
Find the generated API token with sudo cat /var/lib/opex-flow/api_token (see
Security).
Docker
cp apps/connector/config.example.yaml config.yaml # set connector.health_check_port: 3000
printf 'CONNECTOR_HMAC_SECRET=%s\nCONNECTOR_WEBHOOK_TOKEN=...\nCONNECTOR_API_TOKEN=%s\n' \
"$(openssl rand -hex 32)" "$(openssl rand -hex 32)" > .env
GH_TOKEN=ghp_xxx docker build --secret id=gh_token,env=GH_TOKEN -t opex-flow .
docker compose up -dThe image runs as non-root user app, binds 0.0.0.0 inside the container (the compose
file publishes the port on host loopback only), and keeps all state in the /data volume.
With no config mounted it starts from a built-in receiver-ready default (/etc/opex-flow/config.yaml).
The source build needs a read-only GitHub token because opex-contracts is a private dependency.
Without a compiler or token, build the same image from the published static release:
docker build --target runtime-release -t opex-flow . (--build-arg OPEX_FLOW_VERSION=2.1.0 --build-arg OPEX_FLOW_SHA256=<digest> to pin and verify; amd64 and arm64). That is what
Hosted deployment uses for a VPS behind a Cloudflare Tunnel.
Edge (ARM64)
The same static binary cross-compiles to aarch64-unknown-linux-musl. Static musl binaries
cannot load Fanuc’s FOCAS library, but they do not need it: FOCAS works there through the
built-in native client (adapters.focas.mode: native, beta). To use Fanuc’s own library on Linux
download the glibc release (linux-x86_64-gnu, built with
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17, runs on glibc 2.17+) or use the
Windows release. Releases include SHA256SUMS, and every
download carries an X-Checksum-Sha256 header; verify before installing.
High availability
For a two-node active-standby pair (lease in NATS KV or Postgres, /api/ready load-balancer
check, failover in about one lease TTL) see High availability.