Skip to main content

Configuration

Both lplex-server and lplex-cloud support HOCON configuration files. CLI flags always override config file values.

Config file discovery

lplex-server looks for config files in this order:

  1. Path specified with -config /path/to/file
  2. ./lplex-server.conf (current directory)
  3. /etc/lplex-server/lplex-server.conf

lplex-cloud uses the same pattern:

  1. -config /path/to/file
  2. ./lplex-cloud.conf
  3. /etc/lplex-cloud/lplex-cloud.conf

lplex-server (boat server)

Full annotated config

# CAN interface name (single bus)
interface = can0

# Multi-bus: list of CAN interfaces (overrides 'interface' when set)
# interfaces = ["can0", "can1"]

# Use in-memory loopback buses instead of SocketCAN (for development on macOS)
# loopback = false

# HTTP listen port
port = 8089

# Maximum buffer duration for buffered client sessions (ISO 8601)
max-buffer-duration = PT5M

# Ring buffer size in entries (must be power of 2, default 65536).
# Each entry holds one reassembled CAN frame. At 100 frames/sec,
# 65536 entries ≈ 10 minutes of history.
# ring-size = 65536

# Alert after this duration with no CAN frames (ISO 8601)
bus-silence-timeout = PT30S

device {
# Remove devices not seen for this duration (Go duration, 0 = disabled)
idle-timeout = 5m
}

health {
# Health check reports unhealthy after this silence duration
bus-silence-threshold = PT30S
}

send {
# Enable /send and /query HTTP endpoints (default: false).
# With no rules, all PGNs and destinations are allowed.
enabled = true

# Ordered rules evaluated top-to-bottom; first match wins.
# No matching rule = deny. Empty list + enabled = allow all.
#
# Rules can be strings (DSL syntax) or native HOCON objects:
# String: [!] [pgn:<spec>] [name:<hex>,...]
# Object: { deny = true/false, pgn = "<spec>", name = "<hex>" or [...] }
rules = [
"!pgn:65280-65535" # string: deny proprietary PGNs
"pgn:59904" # string: allow ISO Request
{ pgn = "126208", name = "001c6e4000200000" } # object: allow command to device
{ pgn = "129025-129029", name = [ # object: PGN range + device list
"001c6e4000200000"
"001c6e4000200001"
]}
]
}

journal {
# Directory for .lpj journal files (empty = disabled)
dir = /var/log/lplex

# Filename prefix
prefix = nmea2k

# Block size in bytes (power of 2, min 4096)
block-size = 262144

# Compression: none, zstd, zstd-dict
compression = zstd

rotate {
# Rotate after this duration (ISO 8601, 0 = disabled)
duration = PT1H

# Rotate after this many bytes (0 = disabled)
size = 0
}

retention {
# Delete files older than this (ISO 8601, 0 = disabled)
max-age = P30D

# Keep at least this much data even if over max-age (ISO 8601)
min-keep = PT24H

# Hard cap on total journal size in bytes (0 = disabled)
max-size = 10737418240

# Percentage of max-size for proactive archiving (0-100)
soft-pct = 80

# What to do when hard cap is hit and archives failed:
# delete-unarchived or pause-recording
overflow-policy = delete-unarchived
}

archive {
# Path to archive script (empty = disabled)
command = /usr/local/bin/archive-to-s3

# When to archive: on-rotate or before-expire
trigger = on-rotate
}
}

virtual-device {
# Claims a source address on the CAN bus so that frames sent via
# /send and /query come from a legitimate NMEA 2000 participant.
# Without this, some devices ignore frames from the unclaimed address 254.
# Source address is auto-selected (highest free, counting down from 252).
enabled = true

# 64-bit hex ISO NAME (required). Lower values win address conflicts.
name = "00e0170001000004"

# Product info model ID (default: lplex-server)
model-id = "lplex-server"

# How often to re-broadcast address claim (PGN 60928) on the bus.
# Keeps the bus aware we're alive and re-asserts address ownership.
claim-heartbeat = "60s"

# How often to re-broadcast product info (PGN 126996).
# Longer interval since it's a 134-byte fast-packet.
product-info-heartbeat = "5m"
}

clients {
# Pre-configured buffered sessions created at startup.
# Clients can connect via GET /clients/{id}/events without a prior PUT.
slots = [
{
id = "dashboard"
buffer-timeout = "PT5M"
}
{
id = "nav-only"
buffer-timeout = "PT2M"
filter {
pgn = [129025, 129026, 129029]
}
}
]
}

replication {
# Cloud server gRPC address (empty = disabled)
target = "cloud.example.com:9443"

# Instance identifier (must match mTLS cert CN)
instance-id = boat-001

tls {
# Client certificate for mTLS
cert = /etc/lplex-server/client.crt

# Client private key
key = /etc/lplex-server/client.key

# CA certificate to verify cloud server
ca = /etc/lplex-server/ca.crt
}
}

CLI flag reference

FlagHOCON PathDefaultDescription
-interfaceinterfacecan0SocketCAN interface (single bus)
-interfacesinterfaces(empty)Comma-separated CAN interfaces for multi-bus (overrides -interface)
-loopbackloopbackfalseUse in-memory loopback buses instead of SocketCAN (for macOS development)
-portport8089HTTP listen port
-max-buffer-durationmax-buffer-durationPT5MMax buffer timeout for sessions
-ring-sizering-size65536Ring buffer entries (power of 2)
-bus-silence-timeoutbus-silence-timeoutPT30SAlert on bus silence
-bus-silence-thresholdhealth.bus-silence-thresholdPT30SHealth check silence threshold
-device-idle-timeoutdevice.idle-timeout5mRemove devices not seen for this duration (0 = disabled)
-send-enabledsend.enabledfalseEnable /send and /query endpoints
-send-rulessend.rules(empty)Semicolon-separated send rules (HOCON: string or object array)
-journal-dirjournal.dir(empty)Journal directory
-journal-prefixjournal.prefixnmea2kJournal file prefix
-journal-block-sizejournal.block-size262144Block size (bytes)
-journal-compressionjournal.compressionzstdCompression type
-journal-rotate-durationjournal.rotate.durationPT1HRotation interval
-journal-rotate-sizejournal.rotate.size0Rotation size
-journal-retention-max-agejournal.retention.max-ageP30DMaximum file age
-journal-retention-min-keepjournal.retention.min-keepPT24HMinimum kept data
-journal-retention-max-sizejournal.retention.max-size0Size hard cap
-journal-retention-soft-pctjournal.retention.soft-pct80Soft threshold %
-journal-retention-overflow-policyjournal.retention.overflow-policydelete-unarchivedOverflow behavior
-journal-archive-commandjournal.archive.command(empty)Archive script path
-journal-archive-triggerjournal.archive.triggeron-rotateArchive trigger
-virtual-devicevirtual-device.enabledfalseEnable virtual NMEA 2000 device for address claiming
-virtual-device-namevirtual-device.name(empty)64-bit hex ISO NAME (required when enabled)
-virtual-device-model-idvirtual-device.model-idlplex-serverProduct info model ID
-virtual-device-claim-heartbeatvirtual-device.claim-heartbeat60sAddress claim re-broadcast interval
-virtual-device-product-info-heartbeatvirtual-device.product-info-heartbeat5mProduct info re-broadcast interval
-replication-targetreplication.target(empty)Cloud gRPC address
-replication-instance-idreplication.instance-id(empty)Instance ID
-replication-tls-certreplication.tls.cert(empty)Client TLS cert
-replication-tls-keyreplication.tls.key(empty)Client TLS key
-replication-tls-careplication.tls.ca(empty)CA cert
-otel-endpointotel.endpoint(empty)OTLP gRPC collector for distributed tracing
-otel-sample-ratiootel.sample-ratio1.0Trace sampling ratio (0.0-1.0)
-alert-webhook-urlalert.webhook-url(empty)HTTP POST endpoint for alert notifications
-alert-dedup-windowalert.dedup-window5mSuppress duplicate alerts within window
-mqtt-brokermqtt.broker(empty)MQTT broker URL (e.g. tcp://localhost:1883)
-mqtt-topic-prefixmqtt.topic-prefixlplexMQTT topic prefix for published frames
-mqtt-client-idmqtt.client-idlplex-serverMQTT client ID
-mqtt-qosmqtt.qos0MQTT QoS level (0, 1, or 2)
-mqtt-usernamemqtt.username(empty)MQTT broker username
-mqtt-passwordmqtt.password(empty)MQTT broker password
-read-onlyread-onlyfalseDisable /send and /query entirely (defense in depth)
-send-rate-limitsend.rate-limit0Max requests/sec for /send and /query (0 = unlimited)
-send-rate-burstsend.rate-burst10Max burst size for /send rate limiter
-api-keyapi-key(empty)API key for HTTP authentication (empty = no auth)

lplex-cloud

Full annotated config

# --- ACME mode (recommended for public internet) ---
# Single port with automatic Let's Encrypt certificates
listen = ":443"
acme {
domain = "lplex.dockwise.app"
email = "admin@example.com"
}
tls {
# CA cert for verifying client mTLS certificates
client-ca = "/etc/lplex-cloud/ca.crt"
}

# --- OR: Dual-port mode (for private networks) ---
grpc {
listen = ":9443"
tls {
cert = "/etc/lplex-cloud/server.crt"
key = "/etc/lplex-cloud/server.key"
client-ca = "/etc/lplex-cloud/ca.crt"
}
}
http {
listen = ":8080"
}

# Instance state and journal storage
data-dir = "/data/lplex"

# Ring buffer size per instance in entries (must be power of 2, default 65536)
# ring-size = 65536

# Same retention/archive config as lplex-server
journal {
# Rotate live journal files after this duration or size (whichever comes first).
# Required for on-rotate archival to work (files must rotate to trigger archival).
rotate-duration = PT1H
# rotate-size = 0 # bytes, 0 = disabled

retention {
max-age = P90D
max-size = 107374182400
soft-pct = 80
overflow-policy = delete-unarchived
}
archive {
command = /usr/local/bin/archive-to-s3
trigger = on-rotate
}
}

Cloud CLI flag reference

FlagHOCON PathDefaultDescription
-listenlisten:443ACME mode listen address
-acme-domainacme.domain(empty)Let's Encrypt domain
-acme-emailacme.email(empty)ACME account email
-grpc-listengrpc.listen:9443gRPC listen (dual-port mode)
-http-listenhttp.listen:8080HTTP listen (dual-port mode)
-tls-certgrpc.tls.cert(empty)Server TLS cert
-tls-keygrpc.tls.key(empty)Server TLS key
-tls-client-cagrpc.tls.client-ca / tls.client-ca(empty)Client CA cert
-data-dirdata-dir/data/lplexData directory
-ring-sizering-size65536Ring buffer entries per instance (power of 2)
-device-idle-timeoutdevice.idle-timeout5mRemove devices not seen for this duration (0 = disabled)
-journal-rotate-durationjournal.rotate-durationPT1HRotate live journal files after this duration (ISO 8601)
-journal-rotate-sizejournal.rotate-size0Rotate live journal files after this many bytes (0 = disabled)

Retention and archive flags are the same as lplex-server (see table above).

Systemd

The .deb package installs a systemd unit at /lib/systemd/system/lplex-server.service. You can override settings via environment variables in /etc/default/lplex-server:

# /etc/default/lplex-server
LPLEX_ARGS="-interface can0 -port 8089"

Or use the config file at /etc/lplex-server/lplex-server.conf (preferred).

# Check service status
sudo systemctl status lplex-server

# View logs
sudo journalctl -u lplex-server -f

# Restart after config changes
sudo systemctl restart lplex-server

Config validation

lplex-server -validate-config parses and validates the configuration without starting the server. It works with -config path or auto-discovered config files, and checks all durations, sizes, enum values, send rules, and virtual device config. Each setting is reported as [OK], [FAIL], or [WARN].

Exit code 0 means the config is valid; exit code 1 means errors were found.

lplex-server -validate-config -config /etc/lplex/lplex-server.conf

Duration format

All duration values use ISO 8601 duration format:

ExampleMeaning
PT30S30 seconds
PT5M5 minutes
PT1H1 hour
PT24H24 hours
P1D1 day
P30D30 days
P90D90 days