---
title: Deployment
slug: solutions/deployment
docTags: 
createdAt: 2026-08-31T22:44:54.520Z
---

The node runs as a container in either of two modes. Inside Litmus Edge it finds the instance on its own, needing no hostname and no NATS credentials — the normal production deployment. Outside Litmus Edge it reads the same devices and publishes the same Sparkplug messages, but you supply the Litmus Edge address and credentials for its NATS broker.

[Quick Start](#) covers the Marketplace path and the verification steps; this page covers everything else.

## Inside Litmus Edge

Use these steps when you manage the container yourself rather than through the Marketplace.

![](https://api.archbee.com/api/optimize/SSUUxKZUk9bFTEPNn_6Zo/i6__T2EB2z0_SBr0s7mVj_image.png)

1. Download the image archive from [Litmus Central](https://portal.litmus.io/accelerators/le-sparkplug-edge-node?tab=downloads), or pull it from the registry if you have registry access.
2. In Litmus Edge, navigate to Applications > Images, then upload and import the archive.
3. Navigate to Applications > Containers, click Run, and enter the command. See [Overview of docker run](https://docs.litmus.io/litmusedge/product-features/applications/containers/overview-of-docker-run).

```bash
docker run -d --name le-sparkplug --restart unless-stopped \
  -v le-sparkplug-data:/le-sparkplug/data \
  -e EDGE_API_TOKEN="<Litmus Edge API token>" \
  -e GROUP_ID="Plant1" \
  -e NODE_ID="Line1" \
  -e MQTT_URL="tcp://192.168.1.20:1883" \
  le-sparkplug:v2.0.1
```

With registry access, use the registry path in place of the local image name: `us-docker.pkg.dev/litmus-customer-facing/litmus-solutions/le-sparkplug:v2.0.1`. There is no `latest` tag, so the version is always explicit.

The node reaches Litmus Edge through the Docker gateway, which it detects from the container's default route. Set `EDGE_DOCKER_GATEWAY_IP` to name that gateway explicitly when detection does not find the right one.

Add `-p 9090:9090` to reach the metrics and health endpoints from outside the container.

## Outside Litmus Edge

External mode adds two things to the deployment above: the Litmus Edge address, and credentials for its NATS broker. Beyond the [Quick Start prerequisites](#), it needs:

- Litmus Edge reachable from the container host by hostname or IP address
- An API Account, whose API key gives the node access to the Litmus Edge NATS broker. See [Create an API Account](https://docs.litmus.io/litmusedge/product-features/system/access-control/tokens/create-api-account)
- The NATS proxy enabled on the Litmus Edge instance. See [Tokens](https://docs.litmus.io/litmusedge/product-features/system/access-control/tokens)

```bash
docker run -d --name le-sparkplug --restart unless-stopped \
  -v le-sparkplug-data:/le-sparkplug/data \
  -e EDGE_EXTERNAL="true" \
  -e EDGE_HOSTNAME="edge-01.plant.example.com" \
  -e EDGE_API_TOKEN="<Litmus Edge API token>" \
  -e EDGE_ACCESS_ACCOUNT_API_KEY="<API Account API key>" \
  -e GROUP_ID="Plant1" \
  -e NODE_ID="Line1" \
  -e MQTT_URL="tcp://192.168.1.20:1883" \
  us-docker.pkg.dev/litmus-customer-facing/litmus-solutions/le-sparkplug:v2.0.1
```

| Setting                       | Effect                                                                                                |
| ----------------------------- | ----------------------------------------------------------------------------------------------------- |
| `EDGE_EXTERNAL=true`          | Switches to external mode, where the Litmus Edge address must be given explicitly                     |
| `EDGE_HOSTNAME`               | Hostname or IP address of the Litmus Edge instance. Required in external mode                         |
| `EDGE_ACCESS_ACCOUNT_API_KEY` | API Account key. The node builds the NATS connection `tls://admin:<key>@<EDGE_HOSTNAME>:4222` from it |

The generated connection always uses port 4222, so a port in `EDGE_HOSTNAME` applies to the API only. Keys containing characters such as `/` or a space need no escaping — the node encodes them.

### Connect with an Explicit NATS URL

Set `NATS_URL` when the generated connection does not fit, such as a non-default port or a user other than `admin`. It takes precedence over `EDGE_ACCESS_ACCOUNT_API_KEY`, which is then unnecessary.

```bash
-e NATS_URL="tls://admin:<API Account API key>@edge-01.plant.example.com:4222"
```

The scheme decides encryption: `tls://` connects over TLS, `nats://` connects without it. `EDGE_HOSTNAME` is still required, because the node reaches the Litmus Edge API through it — only the NATS connection comes from `NATS_URL`.

## Persist the Data Directory

The node keeps its Sparkplug state in `/le-sparkplug/data`. Every deployment path mounts the Docker volume `le-sparkplug-data` there, which is also what the Marketplace application does.

| Path in the container             | Contents                                                   |
| --------------------------------- | ---------------------------------------------------------- |
| `seqNum/<group>-<node>_bdSeq.txt` | The birth/death sequence number for the next MQTT session  |
| `<group>-<node>_db/`              | The store-and-forward buffer, when that feature is enabled |
| `config/config.json`              | The configuration file, when you use one                   |

A configuration file goes at `config/config.json` inside the volume. See [Configuration](#) for the file format.

## Upgrade to a New Version

State lives in the volume, not the container, so an upgrade replaces the container and keeps the volume.

1. Note the Sparkplug Group ID, Node ID, and volume name of the running container, then stop and remove it.
2. Deploy the new version with the same Group ID, Node ID, and volume name.
3. Confirm the handover on the broker: `NDEATH` from the old session, then `NBIRTH` from the new one, carrying the next birth/death sequence number.

## Secure the MQTT Connection with TLS

The node connects over TLS when the broker URL uses the `ssl://` or `mqtts://` scheme and you supply the certificate authority that signed the broker's certificate. Add a client certificate and key when the broker requires mutual TLS.

Certificates are supplied as Base64-encoded text, not file paths, because the node reads them from settings rather than from disk.

### Encode the Certificates

Encode each PEM file, including its `-----BEGIN-----` and `-----END-----` lines, as a single line of Base64.

```bash
base64 < ca.pem | tr -d '\n'
base64 < client.crt | tr -d '\n'
base64 < client.key | tr -d '\n'
```

```powershell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("ca.pem"))
[Convert]::ToBase64String([IO.File]::ReadAllBytes("client.crt"))
[Convert]::ToBase64String([IO.File]::ReadAllBytes("client.key"))
```

The output must be one unbroken string. A Base64 value with line breaks in it fails to decode.

### Configure the Connection

| Purpose                | Marketplace parameter   | Environment variable    |
| ---------------------- | ----------------------- | ----------------------- |
| Broker URL, TLS scheme | MQTT Broker URL         | `MQTT_URL`              |
| CA certificate         | MQTT CA Certificate     | `MQTT_CA_CERT`          |
| Client certificate     | MQTT Client Certificate | `MQTT_CLIENT_CERT`      |
| Client private key     | MQTT Client Key         | `MQTT_CLIENT_KEY`       |
| Skip verification      | not exposed             | `MQTT_SKIP_CERT_VERIFY` |

The equivalent `config.json` fields sit on each broker object — see [Configuration](#).

The URL scheme decides whether the connection is encrypted at all: `ssl://` and `mqtts://` use TLS, `tcp://` and `mqtt://` do not. No other schemes are accepted, so a `tls://` broker URL is rejected at startup.

`MQTT_SKIP_CERT_VERIFY` turns off verification of the broker's certificate. The connection stays encrypted, but the node accepts any certificate the broker presents. It is deliberately absent from the Marketplace form, so setting it takes a configuration file or a self-managed container.

### Multiple Brokers with Different Authorities

A configuration file carries one certificate set per broker, which is the reason to prefer a file when brokers have different certificate authorities.

```json
{
  "edge_api_token": "<Litmus Edge API token>",
  "group_id": "Plant1",
  "node_id": "Line1",
  "mqtt": {
    "servers": [
      {
        "url": "ssl://broker-a.example.com:8883",
        "user": "sparkplug",
        "password": "<password>",
        "ca_cert": "<Base64 CA certificate>",
        "client_cert": "<Base64 client certificate>",
        "client_key": "<Base64 client key>"
      },
      {
        "url": "ssl://broker-b.example.com:8883",
        "ca_cert": "<Base64 CA certificate>"
      }
    ]
  }
}
```

The node publishes to one broker at a time and moves to the next in the list when the session ends.

### Certificate Errors

A successful TLS connection logs `connected` from the `mqtt` component with the `ssl://` URL in the `server` field, followed by `birth set published`. Certificate problems are logged and do not stop the node from starting:

| Message from `mqtt`               | Meaning                                                                                                                 |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `decode CA_CERT base64`           | The CA value is not valid Base64. The node continues with the system trust store, and the client certificate is skipped |
| `decode CLIENT_CERT base64`       | The client certificate value is not valid Base64                                                                        |
| `decode CLIENT_KEY base64`        | The client key value is not valid Base64                                                                                |
| `load client certificate and key` | The pair decoded but do not form a usable key pair, so the certificate and key do not match                             |

The first three are most often caused by line breaks surviving the encoding.

## Verify the Deployment

Follow [Quick Start](#) steps 2 and 3 — the log messages and broker topics are the same in both modes. Outside Litmus Edge, read the logs with `docker logs le-sparkplug` and add `-p 9090:9090` to reach the health endpoint:

```bash
curl -i http://127.0.0.1:9090/healthz
```

`200 ok` means the MQTT session is up. `503 unavailable` means it is not, or that host application gating is enabled and the host has not reported online yet.

In external mode, if the `nats` component never logs `connected`, the API Account key or the NATS proxy is the place to look. If `nats` connects but the birth set reports no devices, the node reached the NATS broker while the API returned nothing — check the API token and the hostname.

The generated NATS connection is encrypted, but the node does not verify the Litmus Edge certificate, because Litmus Edge presents a self-signed certificate. The same applies to its calls to the Litmus Edge API.Sparkplug requires the birth/death sequence number to keep advancing from one session to the next. Lose this volume and the node restarts that counter, so host applications read the new session as out of order and mark the node stale. Keep the volume, and its name, across upgrades.Never run two nodes with the same Sparkplug Group ID and Node ID, and never point two containers at the same volume. Either one puts two Sparkplug identities on the same topics and corrupts the sequence-number state.The client certificate and key are read only when a CA certificate is also set. Supplying a client pair on its own leaves the connection without client authentication, and nothing in the configuration is rejected.Skipping verification removes protection against an attacker impersonating your broker. Use it only to isolate a certificate problem in testing, and never in production. The fix in production is to supply the correct CA certificate.
