Skip to main content

Run it as a service

A native install started from a terminal stops when the terminal closes. On a gateway that has to survive reboots, register it with the operating system's service manager instead.

This page assumes you have already followed Install from npm and that the gateway runs correctly in the foreground. Get that working first — debugging a configuration problem through a service manager is needlessly painful.

Why the stop path matters

Omni-Edge releases its licence seat while shutting down. It handles SIGINT, SIGTERM, SIGHUP and SIGBREAK, runs its cleanup hooks, and exits. The whole teardown is time-bounded (8 seconds by default, overridable with SHUTDOWN_GRACE_MS).

Which of those actually arrives depends on the platform, and Windows is the awkward one:

SignalOn LinuxOn Windows
SIGINTCtrl+CCtrl+C — delivered
SIGBREAKCtrl+Break — delivered
SIGTERMsystemctl stop, docker stopnever delivered — Windows has no mechanism for it
SIGHUPterminal hanguponly on console-window close, never on a service stop

So on Linux SIGTERM is the stop path, and on Windows it is the two console events. Both are handled.

So a service definition has one requirement beyond the obvious: give the process enough time to stop. A supervisor that sends a signal and kills the process a second later will cut the licence release short every time. The settings below allow 45 seconds, which is generous.

Nothing is lost if a stop is cut short — a hard kill leaves the seat bound to this machine's stable identity, so the next start re-uses it rather than consuming a new one. A clean stop is what returns a floating seat to the pool early.

Linux — systemd

Create a dedicated user and put the installation somewhere predictable:

sudo useradd --system --create-home --home-dir /opt/omni-edge omniedge
sudo chown -R omniedge:omniedge /opt/omni-edge

Write /etc/systemd/system/opcua-omni-edge.service:

[Unit]
Description=OPC UA Omni-Edge gateway
Documentation=https://opcua-omni-edge.doc.sterfive.com/
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=omniedge
Group=omniedge
WorkingDirectory=/opt/omni-edge
ExecStart=/opt/omni-edge/node_modules/.bin/opcua-omni-edge run -c /opt/omni-edge/config.yaml

# The gateway stores its licence activation and stable machine identity
# under $HOME/.sterfive/opcua-omni-edge. systemd does not set HOME for
# system services, so pin it: without this the identity can land
# somewhere unexpected and a restart consumes a new licence seat.
Environment=HOME=/opt/omni-edge

Restart=on-failure
RestartSec=5

# Let the licence release finish. KillSignal is SIGTERM by default, which
# the gateway handles; TimeoutStopSec must comfortably exceed the 8s
# internal shutdown budget.
KillSignal=SIGTERM
TimeoutStopSec=45

[Install]
WantedBy=multi-user.target

Enable and start it:

sudo systemctl daemon-reload
sudo systemctl enable --now opcua-omni-edge
systemctl status opcua-omni-edge
journalctl -u opcua-omni-edge -f

On a clean systemctl stop, the journal shows licence lease released. If it does not, the stop was cut short — check TimeoutStopSec.

Hardening

Once it works, the usual systemd confinement applies. Add to [Service]:

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/omni-edge

ReadWritePaths must cover the installation folder, the certificate store, and $HOME/.sterfive — the gateway writes to all three.

Windows — WinSW

WinSW wraps any executable as a Windows service from a single XML file. Download WinSW.exe into the installation folder and rename it to match the config file — for example omni-edge-service.exe and omni-edge-service.xml.

<service>
<id>opcua-omni-edge</id>
<name>OPC UA Omni-Edge</name>
<description>Sterfive OPC UA Omni-Edge gateway</description>

<executable>node</executable>
<arguments>node_modules\@sterfive\opcua-omni-edge\opcua-omni-edge.js run -c config.yaml</arguments>
<workingdirectory>C:\omni-edge</workingdirectory>

<onfailure action="restart" delay="10 sec"/>
<stoptimeout>45 sec</stoptimeout>

<log mode="roll-by-size">
<sizeThreshold>10240</sizeThreshold>
<keepFiles>3</keepFiles>
</log>
</service>

Install and start it from an elevated prompt:

.\omni-edge-service.exe install
.\omni-edge-service.exe start
.\omni-edge-service.exe status

WinSW stops a console application by raising a console control event, which is exactly what the gateway needs — Ctrl+Break arrives as SIGBREAK and the cleanup hooks run. Confirm it once after installing, because a wrapper that terminates the process instead would skip the release silently:

.\omni-edge-service.exe stop

A clean stop logs both of these lines:

shutting down (SIGBREAK) ...
shutdown complete.

SIGINT in place of SIGBREAK is equally good — it means the wrapper used Ctrl+C. What matters is that the lines are there. If they are missing and the process exit code is -1073741510 (0xC000013A, STATUS_CONTROL_C_EXIT), the process was killed before any hook ran; raise <stoptimeout> and re-test.

info

The licence activation and stable machine identity live under %USERPROFILE%\.sterfive\opcua-omni-edge, and %USERPROFILE% resolves for the account the service runs as — the Windows counterpart of the Environment=HOME= pin in the systemd unit above. A service running as LocalSystem looks in C:\Windows\system32\config\systemprofile, so an activation performed while logged in as yourself is invisible to it and the gateway starts unlicensed.

Either run the service as the account that activated it, or supply the key by environment variable — OPCUA_OMNI_EDGE_ACTIVATION_KEY_FILE pointing at a file the service account can read — which is account-independent and the better choice for unattended installs.

Do not use the .cmd shim as the <executable>: it introduces an intermediate cmd.exe that the service manager stops instead of the gateway. Call node on the package's entry point, as above.

A note on pm2

pm2 is a popular Node process manager, and pm2 startup will generate the boot registration for you. It is a reasonable choice on a development or pilot machine, where one command works the same on every OS.

It is not what we recommend for production, for two reasons:

  • pm2's default kill_timeout is 1600 ms, well under the gateway's 8-second shutdown budget — it will SIGKILL mid-release on every restart. If you use pm2, set kill_timeout: 30000 in the ecosystem file.
  • On Windows, pm2 cannot deliver signals at all; it terminates the process. The cleanup hooks never run, and unlike WinSW there is no configuration that changes this — the seat stays bound to the machine and is re-used on the next start, but it is never released early.

systemd is already present on any Linux gateway, needs no extra daemon, and gives exact control over the stop path. That is the difference that matters here.