signalk-container 1.15.0 → 1.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +470 -456
- package/dist/doctor.d.ts +6 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +24 -0
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +7 -1
- package/dist/jobs.js.map +1 -1
- package/package.json +1 -1
- package/public/remoteEntry.js +1 -1
- package/public/chunks/module-runner-DgpG11zr.js +0 -22
- package/public/chunks/ssrEntryLoader-LO6v5LYY.js +0 -2
package/README.md
CHANGED
|
@@ -27,6 +27,255 @@ Instead of each plugin implementing its own container orchestration, they delega
|
|
|
27
27
|
- **Docker `host.containers.internal` parity** -- signalk-container adds the `host-gateway` mapping for Docker automatically (Podman has it natively). User-supplied `extraHosts` overrides are respected.
|
|
28
28
|
- **Cross-plugin API** -- other plugins use `globalThis.__signalk_containerManager`
|
|
29
29
|
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Node.js >= 22
|
|
33
|
+
- Podman or Docker installed on the host
|
|
34
|
+
- Signal K server
|
|
35
|
+
|
|
36
|
+
## Running Signal K in a Container
|
|
37
|
+
|
|
38
|
+
If your Signal K server itself runs inside a container (Docker, Podman),
|
|
39
|
+
this plugin needs access to the host's container runtime to manage other
|
|
40
|
+
containers. The plugin auto-detects this scenario via `/.dockerenv` or
|
|
41
|
+
`/run/.containerenv` and prefixes the status with `(in-container)`.
|
|
42
|
+
|
|
43
|
+
For the plugin to work, two things must be true inside the Signal K
|
|
44
|
+
container:
|
|
45
|
+
|
|
46
|
+
1. **A matching runtime CLI is available** — the CLI inside the SK
|
|
47
|
+
container must match the daemon on the host (Docker host → `docker`;
|
|
48
|
+
Podman host → `podman`). End users typically bind-mount the host
|
|
49
|
+
binary; image maintainers can bake it into a custom image.
|
|
50
|
+
2. **The matching runtime socket is bind-mounted** from the host
|
|
51
|
+
(rootless or rootful podman, or docker).
|
|
52
|
+
|
|
53
|
+
Concrete platform-specific commands — for both end-user and
|
|
54
|
+
image-maintainer setups — are emitted by the deployment doctor (see
|
|
55
|
+
`/api/doctor/deployment` and the snippet generator below). Use those
|
|
56
|
+
as the source of truth; they always reflect the running plugin
|
|
57
|
+
version.
|
|
58
|
+
|
|
59
|
+
### Quick check: `/api/doctor/deployment`
|
|
60
|
+
|
|
61
|
+
The plugin ships a self-diagnostic. After starting, hit:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl http://<signalk-host>:3000/plugins/signalk-container/api/doctor/deployment
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The response includes a `status` field (`ok` / `no-runtime` /
|
|
68
|
+
`socket-unreachable` / `permission-denied` / `self-id-unresolved` /
|
|
69
|
+
`cgroup-controllers-incomplete`) and a `remediation` array of
|
|
70
|
+
copy-pasteable lines for whichever failure mode applies. When startup
|
|
71
|
+
detection fails, the same remediation is also logged to the Signal K
|
|
72
|
+
server log.
|
|
73
|
+
|
|
74
|
+
A separate `cgroupControllers` field on the response reports the
|
|
75
|
+
delegated cgroup v2 controllers and which expected ones are missing —
|
|
76
|
+
see [Cgroup controller delegation](#cgroup-controller-delegation) for
|
|
77
|
+
what missing controllers mean for resource limits.
|
|
78
|
+
|
|
79
|
+
### Generate a starter snippet
|
|
80
|
+
|
|
81
|
+
To bootstrap a new deployment, ask the plugin for a ready-to-paste
|
|
82
|
+
compose fragment or shell command tailored to the detected runtime:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
curl 'http://<signalk-host>:3000/plugins/signalk-container/api/doctor/snippet?format=compose' > docker-compose.yml
|
|
86
|
+
curl 'http://<signalk-host>:3000/plugins/signalk-container/api/doctor/snippet?format=run' > run-signalk.sh
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The endpoint returns plain text by default; pass
|
|
90
|
+
`Accept: application/json` to get the structured
|
|
91
|
+
`SetupSnippetResult` (snippet + Dockerfile sidecar + operator notes)
|
|
92
|
+
for programmatic consumers.
|
|
93
|
+
|
|
94
|
+
> [!note]
|
|
95
|
+
> The per-runtime examples below illustrate the shape of a working setup.
|
|
96
|
+
> For an actual deployment, prefer the doctor-generated snippet above — it
|
|
97
|
+
> reflects your detected runtime and the running plugin version, so it
|
|
98
|
+
> stays correct as these details evolve.
|
|
99
|
+
|
|
100
|
+
### Rootless Podman (recommended)
|
|
101
|
+
|
|
102
|
+
The cleanest setup. Runs as your user, not root, so the security
|
|
103
|
+
exposure is limited to your user account rather than the entire host —
|
|
104
|
+
and matches signalk-container's default behaviour.
|
|
105
|
+
|
|
106
|
+
On the host, ensure the user-scoped podman socket is enabled:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
systemctl --user enable --now podman.socket
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Then in your compose / `podman run`:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
services:
|
|
116
|
+
signalk:
|
|
117
|
+
image: your-signalk-image-with-podman-remote
|
|
118
|
+
user: "${UID}:${GID}" # match the uid that owns the host's podman socket
|
|
119
|
+
volumes:
|
|
120
|
+
- /run/user/${UID}/podman/podman.sock:/run/user/${UID}/podman/podman.sock
|
|
121
|
+
environment:
|
|
122
|
+
- CONTAINER_HOST=unix:///run/user/${UID}/podman/podman.sock
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Your image's Dockerfile should include `podman` or `podman-remote`:
|
|
126
|
+
|
|
127
|
+
```dockerfile
|
|
128
|
+
RUN apt-get update && apt-get install -y podman # Debian/Ubuntu
|
|
129
|
+
# or:
|
|
130
|
+
RUN dnf install -y podman-remote # Fedora/RHEL
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Rootful Podman
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
services:
|
|
137
|
+
signalk:
|
|
138
|
+
image: your-signalk-image-with-podman
|
|
139
|
+
volumes:
|
|
140
|
+
- /run/podman/podman.sock:/run/podman/podman.sock
|
|
141
|
+
environment:
|
|
142
|
+
- CONTAINER_HOST=unix:///run/podman/podman.sock
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Docker
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
services:
|
|
149
|
+
signalk:
|
|
150
|
+
image: your-signalk-image-with-docker-cli
|
|
151
|
+
volumes:
|
|
152
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
153
|
+
group_add:
|
|
154
|
+
- "<docker-gid-from-host>" # `getent group docker | cut -d: -f3`
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
> [!warning]
|
|
158
|
+
> Mounting `/var/run/docker.sock` gives the container **root-equivalent
|
|
159
|
+
> access to the host**. Anyone who compromises Signal K (including via
|
|
160
|
+
> a malicious plugin) can take over the entire host. Prefer rootless
|
|
161
|
+
> Podman for production.
|
|
162
|
+
|
|
163
|
+
### Networking caveats
|
|
164
|
+
|
|
165
|
+
When Signal K runs in a container, containers spawned by this plugin
|
|
166
|
+
are **siblings** on the host's container network, not inside Signal K's
|
|
167
|
+
network namespace. This affects:
|
|
168
|
+
|
|
169
|
+
- The shared `sk-network` works only if Signal K is also attached to it
|
|
170
|
+
(add it externally or via the same compose file)
|
|
171
|
+
- `host.containers.internal` from spawned containers points to the host
|
|
172
|
+
itself, not the Signal K container — use Signal K's container name
|
|
173
|
+
for direct communication. signalk-container 1.8.0+ adds this hostname
|
|
174
|
+
to Docker containers automatically (Podman already provides it); set
|
|
175
|
+
`ContainerConfig.extraHosts` to override it or to add other hostnames.
|
|
176
|
+
|
|
177
|
+
### Cgroup controller delegation
|
|
178
|
+
|
|
179
|
+
When Signal K runs inside a rootless container, the kernel only enforces
|
|
180
|
+
those resource limits whose **cgroup controller has been delegated** down
|
|
181
|
+
to the SK container's cgroup. Anything else passed to `podman run` is
|
|
182
|
+
silently ignored — your `--memory 1g` request reaches the runtime, but
|
|
183
|
+
the cgroup never gets a memory cap and the container can grow without
|
|
184
|
+
bound.
|
|
185
|
+
|
|
186
|
+
The most common culprit is `memory`: many distros delegate `cpu`,
|
|
187
|
+
`cpuset`, `io`, and `pids` to user sessions by default, but `memory`
|
|
188
|
+
must be explicitly added.
|
|
189
|
+
|
|
190
|
+
signalk-container 1.9.0+ probes the available controllers via
|
|
191
|
+
`/sys/fs/cgroup/cgroup.controllers` and **silently drops** unsupported
|
|
192
|
+
limit fields before invoking podman — better than crashing the
|
|
193
|
+
container, but the user-visible effect is "I set memory to 1 GB and
|
|
194
|
+
nothing happened." The drop is logged at `debug` level with the reason
|
|
195
|
+
(`cgroup controller 'memory' not delegated to podman (available: cpuset,
|
|
196
|
+
cpu, io, pids)`); the live `effective` resource response also reflects
|
|
197
|
+
only what's actually in place.
|
|
198
|
+
|
|
199
|
+
**The deployment doctor flags this automatically** with `status:
|
|
200
|
+
cgroup-controllers-incomplete` and a ready-to-paste remediation block.
|
|
201
|
+
Hit `/api/doctor/deployment` (see above) and check the `status` and
|
|
202
|
+
`cgroupControllers` fields.
|
|
203
|
+
|
|
204
|
+
**Manual check from a shell:**
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
podman exec <sk-container> cat /sys/fs/cgroup/cgroup.controllers
|
|
208
|
+
# cpuset cpu io memory pids ← memory delegated, all limits work
|
|
209
|
+
# cpuset cpu io pids ← memory missing, --memory is dropped
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Enable memory delegation on the host** (one-time, requires sudo):
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
sudo mkdir -p /etc/systemd/system/user@.service.d
|
|
216
|
+
sudo tee /etc/systemd/system/user@.service.d/delegate.conf <<'EOF'
|
|
217
|
+
[Service]
|
|
218
|
+
Delegate=cpu cpuset io memory pids
|
|
219
|
+
EOF
|
|
220
|
+
sudo systemctl daemon-reload
|
|
221
|
+
# Log the SK-owning user out and back in (or reboot) so a fresh user@.service starts.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
After re-login, re-running the consumer plugin's `ensureRunning` (or
|
|
225
|
+
just restarting Signal K) recreates the managed container with the
|
|
226
|
+
memory cap actually applied. Verify with `podman inspect sk-<name>
|
|
227
|
+
--format '{{.HostConfig.Memory}}'` — a non-zero value confirms the cap
|
|
228
|
+
is in cgroup state, not just on the command line.
|
|
229
|
+
|
|
230
|
+
This is purely a host-side prerequisite; signalk-container cannot
|
|
231
|
+
override the kernel's controller delegation.
|
|
232
|
+
|
|
233
|
+
#### Raspberry Pi OS: `cgroup_disable=memory` in the kernel cmdline
|
|
234
|
+
|
|
235
|
+
If you're on a Raspberry Pi 4/5 running Raspberry Pi OS Trixie (and
|
|
236
|
+
likely earlier Pi OS releases) and the systemd `Delegate=memory` snippet
|
|
237
|
+
above **doesn't work** — `cat /sys/fs/cgroup/cgroup.controllers` still
|
|
238
|
+
shows `cpuset cpu io pids` after a reboot — the cause is one level
|
|
239
|
+
deeper. The Pi's GPU firmware injects `cgroup_disable=memory` into the
|
|
240
|
+
kernel boot cmdline, so the memory controller never reaches systemd.
|
|
241
|
+
|
|
242
|
+
Quick check:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
grep -o "cgroup_disable=memory" /proc/cmdline
|
|
246
|
+
# Prints "cgroup_disable=memory" → you're hit.
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Full runbook with copy-pasteable commands, verification steps, and
|
|
250
|
+
revert instructions: **[doc/cgroup-memory-on-raspberry-pi-os.md](doc/cgroup-memory-on-raspberry-pi-os.md)**.
|
|
251
|
+
|
|
252
|
+
The deployment doctor at `/api/doctor/deployment` also detects this
|
|
253
|
+
scenario and surfaces the same kernel-cmdline fix in its `remediation`
|
|
254
|
+
array, so you don't have to guess which layer is broken first.
|
|
255
|
+
|
|
256
|
+
### Watch out for systemd auto-restart (Quadlet / `Restart=always`)
|
|
257
|
+
|
|
258
|
+
If you run Signal K via a podman Quadlet (`*.container` in
|
|
259
|
+
`~/.config/containers/systemd/`) or a systemd unit with
|
|
260
|
+
`Restart=always`, the unit silently restarts the SK container within
|
|
261
|
+
`RestartSec` seconds of any stop — including operator-initiated
|
|
262
|
+
`podman stop`. This races with manually-started replacement containers
|
|
263
|
+
on the same port.
|
|
264
|
+
|
|
265
|
+
For test/diagnostic swaps, temporarily disable the unit's
|
|
266
|
+
restart/recovery policy before stopping the container and re-enable it
|
|
267
|
+
afterward. With a `--user` Quadlet (substitute your actual unit name):
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
systemctl --user mask <your-signalk-unit>.service # suppress auto-restart
|
|
271
|
+
# … run your test container on port 3000 …
|
|
272
|
+
systemctl --user unmask <your-signalk-unit>.service # re-enable
|
|
273
|
+
systemctl --user start <your-signalk-unit>.service
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
This is purely an operator-side consideration; signalk-container has no
|
|
277
|
+
visibility into systemd-managed lifecycles.
|
|
278
|
+
|
|
30
279
|
## Config Panel
|
|
31
280
|
|
|
32
281
|
The plugin embeds a React config panel in the Signal K Admin UI (via Module Federation). It's the recommended way to manage containers — you shouldn't need to edit JSON directly.
|
|
@@ -42,7 +291,7 @@ The plugin embeds a React config panel in the Signal K Admin UI (via Module Fede
|
|
|
42
291
|
- **Auto-prune images** -- off, weekly, or monthly scheduled cleanup of dangling images
|
|
43
292
|
- **Update check interval** -- how often to check consumer plugins for new container images (1h to 1 week, default 24h)
|
|
44
293
|
- **Background update checks** -- toggle for metered connections; manual checks still work when off
|
|
45
|
-
- **Disable user-namespace remap (ZFS escape hatch)** -- off by default. Secondary fix for ZFS / id-map-less hosts; prefer the host-side `fuse-overlayfs` storage driver first ([ZFS host notes](#zfs-
|
|
294
|
+
- **Disable user-namespace remap (ZFS escape hatch)** -- off by default. Secondary fix for ZFS / id-map-less hosts; prefer the host-side `fuse-overlayfs` storage driver first ([ZFS host notes](#zfs-and-other-idmap-incompatible-filesystems)). Enable only if container creation fails with `crun: writing file /proc/<pid>/gid_map: Invalid argument` and you cannot switch storage drivers. With the flag on, signalk-container stops emitting `--userns=keep-id` for rootless Podman; bind-mount file ownership still lands on the host caller for root-by-default images (questdb, grafana, mayara), but non-root images lose host-caller ownership in exchange for being able to start at all.
|
|
46
295
|
|
|
47
296
|
### Managed Containers (one card per running or stopped container)
|
|
48
297
|
|
|
@@ -70,141 +319,127 @@ The plugin embeds a React config panel in the Signal K Admin UI (via Module Fede
|
|
|
70
319
|
|
|
71
320
|
- **Prune Dangling Images** button with before/after space reclaimed summary
|
|
72
321
|
|
|
73
|
-
##
|
|
322
|
+
## Setting Resource Limits
|
|
74
323
|
|
|
75
|
-
|
|
76
|
-
const containers = (globalThis as any).__signalk_containerManager;
|
|
77
|
-
if (!containers) {
|
|
78
|
-
app.setPluginError("signalk-container plugin required");
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
324
|
+
On a boat with limited compute (typically a Pi 4/5 or low-power x86 mini PC), one runaway container can starve Signal K, raise NMEA decode latency, trigger thermal throttling, or even take the host down via OOM. signalk-container exposes podman/docker resource flags so consumer plugins can set sensible defaults — and you, as the user, can tune them per-container in two ways: **the config panel UI (recommended)** or direct JSON edit (for scripted/automated setups).
|
|
81
325
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
326
|
+
### How it works
|
|
327
|
+
|
|
328
|
+
Each consumer plugin (signalk-questdb, signalk-grafana, mayara, etc.) declares default CPU/memory limits when it starts its container. Your override is **merged field-by-field** on top of the plugin's defaults, and only the fields that actually differ from the default get stored. This means if a future plugin version bumps its memory default from 512m to 1g, your override for just `cpus` will automatically pick up the new memory value — no manual edit needed.
|
|
329
|
+
|
|
330
|
+
### Using the Config Panel (recommended)
|
|
331
|
+
|
|
332
|
+
1. Open the Signal K admin UI → Plugin Config → **Container Manager**
|
|
333
|
+
2. Find the container you want to tune in the "Managed Containers" list
|
|
334
|
+
3. Click **Edit Limits ▸** on the row
|
|
335
|
+
4. Edit the CPU cores, Memory, Memory+swap, or Max processes fields. Use the × button next to a field to explicitly unset a limit the plugin set. Click **Advanced** to access cpuShares, cpusetCpus, memoryReservation, and oomScoreAdj.
|
|
336
|
+
5. Click **Apply** — live updated where possible (no downtime), recreated where needed. The result box shows which method was used plus any warnings.
|
|
337
|
+
6. To restore the plugin's default: click **Reset to default** (amber button). This clears your override and applies the pristine default to the running container.
|
|
338
|
+
|
|
339
|
+
The form re-seeds from the server's fresh state after every Apply or Reset, so the displayed values always match what's actually running.
|
|
340
|
+
|
|
341
|
+
### Available fields
|
|
342
|
+
|
|
343
|
+
| Field | Example | What it does |
|
|
344
|
+
| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
345
|
+
| `cpus` | `1.5` | Hard CPU cap. `1.5` = max 1.5 cores. The most important field for stability. |
|
|
346
|
+
| `cpuShares` | `512` | Soft CPU weight under contention (default 1024). Lower = lower priority. |
|
|
347
|
+
| `cpusetCpus` | `"1,2"` | Pin to specific cores. Useful to keep heavy containers off core 0 where Signal K runs. May force a recreate on hosts where the cpuset cgroup controller isn't delegated. |
|
|
348
|
+
| `memory` | `"512m"`, `"2g"` | Hard memory cap. Container is OOM-killed if exceeded. |
|
|
349
|
+
| `memorySwap` | `"512m"` | Memory + swap total. **Set equal to `memory` to disable swap entirely** — recommended on Pi/eMMC where swap is slow. |
|
|
350
|
+
| `memoryReservation` | `"256m"` | Soft memory floor. Kernel reclaims first from containers above this. |
|
|
351
|
+
| `pidsLimit` | `200` | Cap on processes/threads. Prevents fork bombs and thread leaks. |
|
|
352
|
+
| `oomScoreAdj` | `500` | OOM kill priority, -1000..1000. Higher = killed first when host runs out of memory. Set at container create time only — forces a recreate when changed. |
|
|
353
|
+
|
|
354
|
+
### Direct JSON (scripted/advanced)
|
|
355
|
+
|
|
356
|
+
The UI writes to a `containerOverrides` map in `plugin-config-data/signalk-container.json`. You can edit this directly if you prefer — useful for automation or bulk configuration:
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"configuration": {
|
|
361
|
+
"containerOverrides": {
|
|
362
|
+
"mayara-server": {
|
|
363
|
+
"cpus": 1.5,
|
|
364
|
+
"memory": "512m",
|
|
365
|
+
"memorySwap": "512m"
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
87
369
|
}
|
|
370
|
+
```
|
|
88
371
|
|
|
89
|
-
|
|
90
|
-
// config against the live container and recreates on drift — no per-
|
|
91
|
-
// plugin hash file or remove() dance needed.
|
|
92
|
-
await containers.ensureRunning("my-service", {
|
|
93
|
-
image: "myorg/myimage",
|
|
94
|
-
tag: "latest",
|
|
95
|
-
signalkDataMount: "/data", // resolves to the SignalK data dir, regardless of deployment
|
|
96
|
-
signalkAccessiblePorts: [8080], // port 8080 in the container must be reachable by SignalK
|
|
97
|
-
env: { MY_VAR: "value" },
|
|
98
|
-
restart: "unless-stopped",
|
|
99
|
-
});
|
|
372
|
+
The key (`mayara-server`) is the container name **without** the `sk-` prefix that signalk-container adds internally. Use `null` for a field to explicitly remove a limit set by the plugin:
|
|
100
373
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const response = await fetch(`http://${addr}/status`);
|
|
374
|
+
```json
|
|
375
|
+
{
|
|
376
|
+
"mayara-server": { "memory": null }
|
|
377
|
+
}
|
|
378
|
+
```
|
|
107
379
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
380
|
+
After editing the file, restart the Container Manager plugin from the Signal K admin UI (or run the REST calls below) for the changes to take effect on running containers.
|
|
381
|
+
|
|
382
|
+
### REST API (for scripts or external tools)
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# Read current state
|
|
386
|
+
curl http://localhost:3000/plugins/signalk-container/api/containers/mayara-server/resources
|
|
387
|
+
|
|
388
|
+
# Apply a new override (live or recreate as needed)
|
|
389
|
+
curl -X POST http://localhost:3000/plugins/signalk-container/api/containers/mayara-server/resources \
|
|
390
|
+
-H 'Content-Type: application/json' \
|
|
391
|
+
-d '{"cpus": 2}'
|
|
392
|
+
|
|
393
|
+
# Reset to plugin default (clear the override)
|
|
394
|
+
curl -X DELETE http://localhost:3000/plugins/signalk-container/api/containers/mayara-server/resources
|
|
116
395
|
```
|
|
117
396
|
|
|
118
|
-
|
|
397
|
+
### When changes take effect
|
|
119
398
|
|
|
120
|
-
|
|
399
|
+
- **Immediately via the UI or REST API** (`updateResources`): signalk-container tries `podman update` / `docker update` first (instantaneous, no downtime). Falls back to stop+remove+create if the runtime can't apply the change live (e.g. unsetting memory limits, or changing `cpusetCpus` / `oomScoreAdj` which are set at container create time only).
|
|
400
|
+
- **On next consumer plugin restart**: the merge happens automatically inside `ensureRunning` — useful for installations that manage via JSON edits and don't want to use the REST API.
|
|
401
|
+
- **Persistence**: overrides applied via the UI or REST API are auto-persisted to `plugin-config-data/signalk-container.json` — they survive Signal K restarts without any extra action.
|
|
121
402
|
|
|
122
|
-
|
|
123
|
-
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
124
|
-
| `getRuntime()` | Returns `{ runtime, version, isRootless, socketPath, ... }` (a `ContainerRuntimeInfo`) or `null` |
|
|
125
|
-
| `whenReady()` | Resolves once runtime detection settles (success OR failure). Replaces the polling-loop pattern; check `getRuntime()` after the await |
|
|
126
|
-
| `pullImage(image, onProgress?)` | Pull a container image (auto-qualifies for Podman) |
|
|
127
|
-
| `imageExists(image)` | Check if image exists locally |
|
|
128
|
-
| `getImageDigest(imageOrContainer)` | Local image ID (sha256) for an image:tag or container |
|
|
129
|
-
| `ensureRunning(name, config, options?)` | Create and start container if not running; auto-recreates on config drift across `image`, `tag`, `command`, `networkMode`, `env`, `volumes`, `ports` |
|
|
130
|
-
| `recreate(name, config, options?)` | Force-recreate: remove (if present) + ensureRunning. Always replaces an existing container (running or stopped) — use this for "update now" / plugin-startup self-heal flows where correctness must not depend on drift detection (1.12.0+) |
|
|
131
|
-
| `start(name)` | Start a stopped container |
|
|
132
|
-
| `stop(name)` | Stop a running container |
|
|
133
|
-
| `remove(name)` | Stop and remove a container |
|
|
134
|
-
| `getState(name)` | Returns `running`, `stopped`, `missing`, or `no-runtime` |
|
|
135
|
-
| `runJob(config)` | Execute a one-shot container job |
|
|
136
|
-
| `cleanupOrphanedJobs(filter)` | Reap `sk-job-*` containers leaked by a previous server lifecycle, filtered by the caller's `ownerPluginId`. Idempotent — returns `{ reaped: OrphanJobInfo[] }` so the plugin can roll back any per-job state it had written |
|
|
137
|
-
| `getLogs(name, options?)` | One-shot fetch of the last N lines of a container's combined stdout+stderr log. `tail` defaults to 200, max 10000; `since` is unix-epoch seconds |
|
|
138
|
-
| `prune()` | Remove dangling images |
|
|
139
|
-
| `listContainers()` | List all `sk-` prefixed containers |
|
|
140
|
-
| `execInContainer(name, command)` | Run a command inside a running container |
|
|
141
|
-
| `ensureNetwork(name)` | Create a Podman/Docker network if it doesn't exist |
|
|
142
|
-
| `removeNetwork(name)` | Remove a network |
|
|
143
|
-
| `connectToNetwork(container, network)` | Add a container to a network (bridge mode only) |
|
|
144
|
-
| `disconnectFromNetwork(container, net)` | Remove a container from a network |
|
|
145
|
-
| `updates.register(reg)` | Register a container for update detection |
|
|
146
|
-
| `updates.unregister(pluginId)` | Stop tracking updates for a plugin |
|
|
147
|
-
| `updates.checkOne(pluginId)` | Force a fresh update check (or coalesce with in-flight) |
|
|
148
|
-
| `updates.getLastResult(pluginId)` | Cached last result, no network |
|
|
149
|
-
| `manifest.get(pluginId)` | Read the persisted manifest for one consumer plugin, or `null` if none. Writes happen automatically after successful `ensureRunning` calls — this is read-only |
|
|
150
|
-
| `manifest.list()` | Return every persisted manifest in the data directory. Order is unspecified |
|
|
151
|
-
| `manifest.getContainerHistory(containerName)` | Bounded history (max 20 entries) of digest changes for a specific container. Throws "Ambiguous container history" if more than one manifest references the same `containerName` — disambiguate via `manifest.get(pluginId)` |
|
|
152
|
-
| `updateResources(name, limits)` | Apply new resource limits live, fall back to recreate |
|
|
153
|
-
| `getResources(name)` | Currently effective limits (plugin defaults ⊕ user override) |
|
|
154
|
-
| `resolveSignalkDataMount()` | Resolve the volume name or host path that backs `app.getDataDirPath()` in the current deployment; returns `null` if the runtime is not yet initialised |
|
|
155
|
-
| `resolveHostPath(absPath)` | Translate an arbitrary absolute path into the `{ source, subPath }` pair the runtime needs to mount it; handles bare-metal, bind, and named-volume topologies |
|
|
156
|
-
| `resolveContainerAddress(name, port)` | Return the `host:port` string to reach `port` on a managed container from the SignalK process; call after `ensureRunning()` with `signalkAccessiblePorts` set |
|
|
157
|
-
| `doctor.imageRunsAsUser(image, user?)` | Probe whether `image` runs cleanly under the host-UID mapping signalk-container will emit (1.8.0+). Never throws — returns `{ ok, output, error? }` |
|
|
158
|
-
| `doctor.selfDeployment()` | Diagnose the Signal K deployment itself: socket resolution, daemon reachability, rootless/rootful detection, and (when containerised) self-container ID. Returns `{ status, remediation, ... }` — see `SelfDeploymentResult` in `src/types.ts` |
|
|
159
|
-
| `doctor.generateSetupSnippet(format?, result?)` | Generate a ready-to-paste compose fragment (`format: "compose"`, default) or `podman/docker run` command (`format: "run"`) tailored to the detected runtime — wires up the socket bind-mount. Pure templating over `SelfDeploymentResult`; includes a `dockerfile` note (no image change needed) and operator notes. |
|
|
403
|
+
### Verifying limits are applied
|
|
160
404
|
|
|
161
|
-
|
|
405
|
+
Check the live container directly:
|
|
162
406
|
|
|
163
|
-
|
|
407
|
+
```bash
|
|
408
|
+
podman inspect sk-mayara-server --format '
|
|
409
|
+
cpus={{.HostConfig.NanoCpus}}
|
|
410
|
+
memory={{.HostConfig.Memory}}
|
|
411
|
+
pids={{.HostConfig.PidsLimit}}
|
|
412
|
+
'
|
|
413
|
+
```
|
|
164
414
|
|
|
165
|
-
|
|
166
|
-
| ------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
167
|
-
| GET | `/runtime` | Detected runtime info |
|
|
168
|
-
| GET | `/containers` | List managed containers |
|
|
169
|
-
| GET | `/containers/:name/state` | Container state |
|
|
170
|
-
| POST | `/containers/:name/start` | Start a stopped container |
|
|
171
|
-
| POST | `/containers/:name/stop` | Stop a running container |
|
|
172
|
-
| POST | `/containers/:name/remove` | Stop and remove a container |
|
|
173
|
-
| GET | `/containers/:name/logs?tail=N&since=ts` | Last N lines of the container's combined stdout+stderr log (one-shot). `tail` defaults 200, max 10000 |
|
|
174
|
-
| GET | `/containers/:name/logs/stream` | Server-Sent Events stream of live log lines. Closes when the container is removed or the client disconnects |
|
|
175
|
-
| POST | `/prune` | Prune dangling images |
|
|
176
|
-
| GET | `/updates` | List last update-check results |
|
|
177
|
-
| GET | `/updates/:pluginId` | Last update-check result for one plugin |
|
|
178
|
-
| POST | `/updates/:pluginId/check` | Force a fresh update check (HTTP 200 even when offline) |
|
|
179
|
-
| GET | `/containers/:name/resources` | Effective resource limits + user override |
|
|
180
|
-
| POST | `/containers/:name/resources` | Apply new resource limits (live or recreate). Body is a `ContainerResourceLimits` diff against the consumer plugin's default. |
|
|
181
|
-
| DELETE | `/containers/:name/resources` | Clear any user override and restore the consumer plugin's pristine default limits to the running container. |
|
|
182
|
-
| POST | `/doctor/image` | Probe whether an image runs cleanly under the live host-UID mapping. Body: `{ image, tag?, user? }`. Never 5xx for a failed probe — `{ ok: false, error }` is a successful response (1.8.0+). |
|
|
183
|
-
| GET | `/doctor/deployment` | Diagnose this Signal K deployment: socket resolution, daemon reachability, rootless/rootful detection, self-container ID cascade. Returns a `SelfDeploymentResult` with `status` and copy-pasteable `remediation` lines. |
|
|
184
|
-
| GET | `/doctor/snippet?format=compose\|run` | Generate a ready-to-paste compose fragment or shell command for setting up Signal K with this runtime. `text/plain` by default; pass `Accept: application/json` for the structured `SetupSnippetResult`. |
|
|
415
|
+
`NanoCpus` is in CPU-nanoseconds per second; `1500000000` = 1.5 cores. Memory is in bytes.
|
|
185
416
|
|
|
186
|
-
|
|
417
|
+
Or via the REST API:
|
|
187
418
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
| Container overrides | `{}` | Per-container resource limits (CPU, memory, PIDs). Field-level merged on top of consumer plugin defaults. See dev guide. |
|
|
419
|
+
```bash
|
|
420
|
+
curl http://localhost:3000/plugins/signalk-container/api/containers/mayara-server/resources | jq
|
|
421
|
+
# {
|
|
422
|
+
# "name": "mayara-server",
|
|
423
|
+
# "effective": { "cpus": 1.5, "memory": "512m", ... }, // what's actually applied
|
|
424
|
+
# "override": { "cpus": 1.5 } // only what the user changed
|
|
425
|
+
# }
|
|
426
|
+
```
|
|
197
427
|
|
|
198
|
-
|
|
428
|
+
Note that `override` contains only the fields that differ from the consumer plugin's default — this minimization is automatic and lets future plugin default bumps flow through without you having to re-edit your override.
|
|
199
429
|
|
|
200
|
-
|
|
430
|
+
### Picking the right values
|
|
201
431
|
|
|
202
|
-
|
|
432
|
+
1. Run the container without overrides for a typical workload
|
|
433
|
+
2. Watch resource use: `podman stats sk-mayara-server`
|
|
434
|
+
3. Note peak CPU% and peak memory
|
|
435
|
+
4. Set `cpus` ≈ peak / 100 + 25% headroom; `memory` ≈ peak rounded up + 25% headroom
|
|
436
|
+
5. Re-test under load to make sure the container still functions inside its caps
|
|
203
437
|
|
|
204
|
-
|
|
205
|
-
2. **Enable the "Disable user-namespace remap" setting** in this plugin's config panel. Escape hatch for hosts that cannot switch storage drivers. Bind-mount ownership stays correct for root-by-default managed containers (questdb, grafana, mayara); non-root images give up host-caller ownership in exchange for being able to start at all.
|
|
438
|
+
The plugin developer guide has a detailed walk-through in [doc/plugin-developer-guide.md#resource-limits](doc/plugin-developer-guide.md#resource-limits).
|
|
206
439
|
|
|
207
|
-
If
|
|
440
|
+
> If you're running Signal K inside a container and a memory limit
|
|
441
|
+
> appears to be ignored, the host's cgroup controller delegation is
|
|
442
|
+
> almost certainly the cause — see [Cgroup controller delegation](#cgroup-controller-delegation) above.
|
|
208
443
|
|
|
209
444
|
## Mounting the SignalK data directory (`signalkDataMount`)
|
|
210
445
|
|
|
@@ -342,370 +577,149 @@ The allocated address is cached for the lifetime of the plugin session, so repea
|
|
|
342
577
|
> a manual `ports` or `networkMode` entry for the same container — the field takes
|
|
343
578
|
> full ownership of those concerns.
|
|
344
579
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
On a boat with limited compute (typically a Pi 4/5 or low-power x86 mini PC), one runaway container can starve Signal K, raise NMEA decode latency, trigger thermal throttling, or even take the host down via OOM. signalk-container exposes podman/docker resource flags so consumer plugins can set sensible defaults — and you, as the user, can tune them per-container in two ways: **the config panel UI (recommended)** or direct JSON edit (for scripted/automated setups).
|
|
348
|
-
|
|
349
|
-
### How it works
|
|
350
|
-
|
|
351
|
-
Each consumer plugin (signalk-questdb, signalk-grafana, mayara, etc.) declares default CPU/memory limits when it starts its container. Your override is **merged field-by-field** on top of the plugin's defaults, and only the fields that actually differ from the default get stored. This means if a future plugin version bumps its memory default from 512m to 1g, your override for just `cpus` will automatically pick up the new memory value — no manual edit needed.
|
|
352
|
-
|
|
353
|
-
### Using the Config Panel (recommended)
|
|
354
|
-
|
|
355
|
-
1. Open the Signal K admin UI → Plugin Config → **Container Manager**
|
|
356
|
-
2. Find the container you want to tune in the "Managed Containers" list
|
|
357
|
-
3. Click **Edit Limits ▸** on the row
|
|
358
|
-
4. Edit the CPU cores, Memory, Memory+swap, or Max processes fields. Use the × button next to a field to explicitly unset a limit the plugin set. Click **Advanced** to access cpuShares, cpusetCpus, memoryReservation, and oomScoreAdj.
|
|
359
|
-
5. Click **Apply** — live updated where possible (no downtime), recreated where needed. The result box shows which method was used plus any warnings.
|
|
360
|
-
6. To restore the plugin's default: click **Reset to default** (amber button). This clears your override and applies the pristine default to the running container.
|
|
361
|
-
|
|
362
|
-
The form re-seeds from the server's fresh state after every Apply or Reset, so the displayed values always match what's actually running.
|
|
363
|
-
|
|
364
|
-
### Available fields
|
|
580
|
+
---
|
|
365
581
|
|
|
366
|
-
|
|
367
|
-
| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
368
|
-
| `cpus` | `1.5` | Hard CPU cap. `1.5` = max 1.5 cores. The most important field for stability. |
|
|
369
|
-
| `cpuShares` | `512` | Soft CPU weight under contention (default 1024). Lower = lower priority. |
|
|
370
|
-
| `cpusetCpus` | `"1,2"` | Pin to specific cores. Useful to keep heavy containers off core 0 where Signal K runs. May force a recreate on hosts where the cpuset cgroup controller isn't delegated. |
|
|
371
|
-
| `memory` | `"512m"`, `"2g"` | Hard memory cap. Container is OOM-killed if exceeded. |
|
|
372
|
-
| `memorySwap` | `"512m"` | Memory + swap total. **Set equal to `memory` to disable swap entirely** — recommended on Pi/eMMC where swap is slow. |
|
|
373
|
-
| `memoryReservation` | `"256m"` | Soft memory floor. Kernel reclaims first from containers above this. |
|
|
374
|
-
| `pidsLimit` | `200` | Cap on processes/threads. Prevents fork bombs and thread leaks. |
|
|
375
|
-
| `oomScoreAdj` | `500` | OOM kill priority, -1000..1000. Higher = killed first when host runs out of memory. Set at container create time only — forces a recreate when changed. |
|
|
582
|
+
# Developer / plugin-author reference
|
|
376
583
|
|
|
377
|
-
|
|
584
|
+
The sections below are for plugin authors integrating with signalk-container. End users deploying the plugin do not need them.
|
|
378
585
|
|
|
379
|
-
|
|
586
|
+
## How Other Plugins Use It
|
|
380
587
|
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
"cpus": 1.5,
|
|
387
|
-
"memory": "512m",
|
|
388
|
-
"memorySwap": "512m"
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
588
|
+
```typescript
|
|
589
|
+
const containers = (globalThis as any).__signalk_containerManager;
|
|
590
|
+
if (!containers) {
|
|
591
|
+
app.setPluginError("signalk-container plugin required");
|
|
592
|
+
return;
|
|
392
593
|
}
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
The key (`mayara-server`) is the container name **without** the `sk-` prefix that signalk-container adds internally. Use `null` for a field to explicitly remove a limit set by the plugin:
|
|
396
594
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
595
|
+
// Wait for runtime detection to settle, then verify a runtime was found.
|
|
596
|
+
await containers.whenReady();
|
|
597
|
+
if (!containers.getRuntime()) {
|
|
598
|
+
app.setPluginError("No container runtime detected");
|
|
599
|
+
return;
|
|
400
600
|
}
|
|
401
|
-
```
|
|
402
601
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
-d '{"cpus": 2}'
|
|
415
|
-
|
|
416
|
-
# Reset to plugin default (clear the override)
|
|
417
|
-
curl -X DELETE http://localhost:3000/plugins/signalk-container/api/containers/mayara-server/resources
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
### When changes take effect
|
|
421
|
-
|
|
422
|
-
- **Immediately via the UI or REST API** (`updateResources`): signalk-container tries `podman update` / `docker update` first (instantaneous, no downtime). Falls back to stop+remove+create if the runtime can't apply the change live (e.g. unsetting memory limits, or changing `cpusetCpus` / `oomScoreAdj` which are set at container create time only).
|
|
423
|
-
- **On next consumer plugin restart**: the merge happens automatically inside `ensureRunning` — useful for installations that manage via JSON edits and don't want to use the REST API.
|
|
424
|
-
- **Persistence**: overrides applied via the UI or REST API are auto-persisted to `plugin-config-data/signalk-container.json` — they survive Signal K restarts without any extra action.
|
|
425
|
-
|
|
426
|
-
### Verifying limits are applied
|
|
427
|
-
|
|
428
|
-
Check the live container directly:
|
|
429
|
-
|
|
430
|
-
```bash
|
|
431
|
-
podman inspect sk-mayara-server --format '
|
|
432
|
-
cpus={{.HostConfig.NanoCpus}}
|
|
433
|
-
memory={{.HostConfig.Memory}}
|
|
434
|
-
pids={{.HostConfig.PidsLimit}}
|
|
435
|
-
'
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
`NanoCpus` is in CPU-nanoseconds per second; `1500000000` = 1.5 cores. Memory is in bytes.
|
|
439
|
-
|
|
440
|
-
Or via the REST API:
|
|
441
|
-
|
|
442
|
-
```bash
|
|
443
|
-
curl http://localhost:3000/plugins/signalk-container/api/containers/mayara-server/resources | jq
|
|
444
|
-
# {
|
|
445
|
-
# "name": "mayara-server",
|
|
446
|
-
# "effective": { "cpus": 1.5, "memory": "512m", ... }, // what's actually applied
|
|
447
|
-
# "override": { "cpus": 1.5 } // only what the user changed
|
|
448
|
-
# }
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
Note that `override` contains only the fields that differ from the consumer plugin's default — this minimization is automatic and lets future plugin default bumps flow through without you having to re-edit your override.
|
|
452
|
-
|
|
453
|
-
### Picking the right values
|
|
454
|
-
|
|
455
|
-
1. Run the container without overrides for a typical workload
|
|
456
|
-
2. Watch resource use: `podman stats sk-mayara-server`
|
|
457
|
-
3. Note peak CPU% and peak memory
|
|
458
|
-
4. Set `cpus` ≈ peak / 100 + 25% headroom; `memory` ≈ peak rounded up + 25% headroom
|
|
459
|
-
5. Re-test under load to make sure the container still functions inside its caps
|
|
460
|
-
|
|
461
|
-
The plugin developer guide has a detailed walk-through in [doc/plugin-developer-guide.md#resource-limits](doc/plugin-developer-guide.md#resource-limits).
|
|
462
|
-
|
|
463
|
-
> If you're running Signal K inside a container and a memory limit
|
|
464
|
-
> appears to be ignored, the host's cgroup controller delegation is
|
|
465
|
-
> almost certainly the cause — see [Cgroup controller delegation](#cgroup-controller-delegation) below.
|
|
466
|
-
|
|
467
|
-
## Requirements
|
|
468
|
-
|
|
469
|
-
- Node.js >= 22
|
|
470
|
-
- Podman or Docker installed on the host
|
|
471
|
-
- Signal K server
|
|
472
|
-
|
|
473
|
-
## Running Signal K in a Container
|
|
474
|
-
|
|
475
|
-
If your Signal K server itself runs inside a container (Docker, Podman),
|
|
476
|
-
this plugin needs access to the host's container runtime to manage other
|
|
477
|
-
containers. The plugin auto-detects this scenario via `/.dockerenv` or
|
|
478
|
-
`/run/.containerenv` and prefixes the status with `(in-container)`.
|
|
479
|
-
|
|
480
|
-
For the plugin to work, two things must be true inside the Signal K
|
|
481
|
-
container:
|
|
482
|
-
|
|
483
|
-
1. **A matching runtime CLI is available** — the CLI inside the SK
|
|
484
|
-
container must match the daemon on the host (Docker host → `docker`;
|
|
485
|
-
Podman host → `podman`). End users typically bind-mount the host
|
|
486
|
-
binary; image maintainers can bake it into a custom image.
|
|
487
|
-
2. **The matching runtime socket is bind-mounted** from the host
|
|
488
|
-
(rootless or rootful podman, or docker).
|
|
489
|
-
|
|
490
|
-
Concrete platform-specific commands — for both end-user and
|
|
491
|
-
image-maintainer setups — are emitted by the deployment doctor (see
|
|
492
|
-
`/api/doctor/deployment` and the snippet generator below). Use those
|
|
493
|
-
as the source of truth; they always reflect the running plugin
|
|
494
|
-
version.
|
|
495
|
-
|
|
496
|
-
### Quick check: `/api/doctor/deployment`
|
|
497
|
-
|
|
498
|
-
The plugin ships a self-diagnostic. After starting, hit:
|
|
499
|
-
|
|
500
|
-
```bash
|
|
501
|
-
curl http://<signalk-host>:3000/plugins/signalk-container/api/doctor/deployment
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
The response includes a `status` field (`ok` / `no-runtime` /
|
|
505
|
-
`socket-unreachable` / `permission-denied` / `self-id-unresolved` /
|
|
506
|
-
`cgroup-controllers-incomplete`) and a `remediation` array of
|
|
507
|
-
copy-pasteable lines for whichever failure mode applies. When startup
|
|
508
|
-
detection fails, the same remediation is also logged to the Signal K
|
|
509
|
-
server log.
|
|
510
|
-
|
|
511
|
-
A separate `cgroupControllers` field on the response reports the
|
|
512
|
-
delegated cgroup v2 controllers and which expected ones are missing —
|
|
513
|
-
see [Cgroup controller delegation](#cgroup-controller-delegation) for
|
|
514
|
-
what missing controllers mean for resource limits.
|
|
515
|
-
|
|
516
|
-
### Generate a starter snippet
|
|
517
|
-
|
|
518
|
-
To bootstrap a new deployment, ask the plugin for a ready-to-paste
|
|
519
|
-
compose fragment or shell command tailored to the detected runtime:
|
|
520
|
-
|
|
521
|
-
```bash
|
|
522
|
-
curl 'http://<signalk-host>:3000/plugins/signalk-container/api/doctor/snippet?format=compose' > docker-compose.yml
|
|
523
|
-
curl 'http://<signalk-host>:3000/plugins/signalk-container/api/doctor/snippet?format=run' > run-signalk.sh
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
The endpoint returns plain text by default; pass
|
|
527
|
-
`Accept: application/json` to get the structured
|
|
528
|
-
`SetupSnippetResult` (snippet + Dockerfile sidecar + operator notes)
|
|
529
|
-
for programmatic consumers.
|
|
530
|
-
|
|
531
|
-
### Rootless Podman (recommended)
|
|
532
|
-
|
|
533
|
-
The cleanest setup. Runs as your user, not root, so the security
|
|
534
|
-
exposure is limited to your user account rather than the entire host —
|
|
535
|
-
and matches signalk-container's default behaviour.
|
|
536
|
-
|
|
537
|
-
On the host, ensure the user-scoped podman socket is enabled:
|
|
538
|
-
|
|
539
|
-
```bash
|
|
540
|
-
systemctl --user enable --now podman.socket
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
Then in your compose / `podman run`:
|
|
544
|
-
|
|
545
|
-
```yaml
|
|
546
|
-
services:
|
|
547
|
-
signalk:
|
|
548
|
-
image: your-signalk-image-with-podman-remote
|
|
549
|
-
user: "${UID}:${GID}" # match the uid that owns the host's podman socket
|
|
550
|
-
volumes:
|
|
551
|
-
- /run/user/${UID}/podman/podman.sock:/run/user/${UID}/podman/podman.sock
|
|
552
|
-
environment:
|
|
553
|
-
- CONTAINER_HOST=unix:///run/user/${UID}/podman/podman.sock
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
Your image's Dockerfile should include `podman` or `podman-remote`:
|
|
557
|
-
|
|
558
|
-
```dockerfile
|
|
559
|
-
RUN apt-get update && apt-get install -y podman # Debian/Ubuntu
|
|
560
|
-
# or:
|
|
561
|
-
RUN dnf install -y podman-remote # Fedora/RHEL
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### Rootful Podman
|
|
565
|
-
|
|
566
|
-
```yaml
|
|
567
|
-
services:
|
|
568
|
-
signalk:
|
|
569
|
-
image: your-signalk-image-with-podman
|
|
570
|
-
volumes:
|
|
571
|
-
- /run/podman/podman.sock:/run/podman/podman.sock
|
|
572
|
-
environment:
|
|
573
|
-
- CONTAINER_HOST=unix:///run/podman/podman.sock
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
### Docker
|
|
577
|
-
|
|
578
|
-
```yaml
|
|
579
|
-
services:
|
|
580
|
-
signalk:
|
|
581
|
-
image: your-signalk-image-with-docker-cli
|
|
582
|
-
volumes:
|
|
583
|
-
- /var/run/docker.sock:/var/run/docker.sock
|
|
584
|
-
group_add:
|
|
585
|
-
- "<docker-gid-from-host>" # `getent group docker | cut -d: -f3`
|
|
586
|
-
```
|
|
587
|
-
|
|
588
|
-
> [!warning]
|
|
589
|
-
> Mounting `/var/run/docker.sock` gives the container **root-equivalent
|
|
590
|
-
> access to the host**. Anyone who compromises Signal K (including via
|
|
591
|
-
> a malicious plugin) can take over the entire host. Prefer rootless
|
|
592
|
-
> Podman for production.
|
|
593
|
-
|
|
594
|
-
### Networking caveats
|
|
595
|
-
|
|
596
|
-
When Signal K runs in a container, containers spawned by this plugin
|
|
597
|
-
are **siblings** on the host's container network, not inside Signal K's
|
|
598
|
-
network namespace. This affects:
|
|
599
|
-
|
|
600
|
-
- The shared `sk-network` works only if Signal K is also attached to it
|
|
601
|
-
(add it externally or via the same compose file)
|
|
602
|
-
- `host.containers.internal` from spawned containers points to the host
|
|
603
|
-
itself, not the Signal K container — use Signal K's container name
|
|
604
|
-
for direct communication. signalk-container 1.8.0+ adds this hostname
|
|
605
|
-
to Docker containers automatically (Podman already provides it); set
|
|
606
|
-
`ContainerConfig.extraHosts` to override it or to add other hostnames.
|
|
607
|
-
|
|
608
|
-
### Cgroup controller delegation
|
|
609
|
-
|
|
610
|
-
When Signal K runs inside a rootless container, the kernel only enforces
|
|
611
|
-
those resource limits whose **cgroup controller has been delegated** down
|
|
612
|
-
to the SK container's cgroup. Anything else passed to `podman run` is
|
|
613
|
-
silently ignored — your `--memory 1g` request reaches the runtime, but
|
|
614
|
-
the cgroup never gets a memory cap and the container can grow without
|
|
615
|
-
bound.
|
|
616
|
-
|
|
617
|
-
The most common culprit is `memory`: many distros delegate `cpu`,
|
|
618
|
-
`cpuset`, `io`, and `pids` to user sessions by default, but `memory`
|
|
619
|
-
must be explicitly added.
|
|
620
|
-
|
|
621
|
-
signalk-container 1.9.0+ probes the available controllers via
|
|
622
|
-
`/sys/fs/cgroup/cgroup.controllers` and **silently drops** unsupported
|
|
623
|
-
limit fields before invoking podman — better than crashing the
|
|
624
|
-
container, but the user-visible effect is "I set memory to 1 GB and
|
|
625
|
-
nothing happened." The drop is logged at `debug` level with the reason
|
|
626
|
-
(`cgroup controller 'memory' not delegated to podman (available: cpuset,
|
|
627
|
-
cpu, io, pids)`); the live `effective` resource response also reflects
|
|
628
|
-
only what's actually in place.
|
|
629
|
-
|
|
630
|
-
**The deployment doctor flags this automatically** with `status:
|
|
631
|
-
cgroup-controllers-incomplete` and a ready-to-paste remediation block.
|
|
632
|
-
Hit `/api/doctor/deployment` (see above) and check the `status` and
|
|
633
|
-
`cgroupControllers` fields.
|
|
634
|
-
|
|
635
|
-
**Manual check from a shell:**
|
|
636
|
-
|
|
637
|
-
```bash
|
|
638
|
-
podman exec <sk-container> cat /sys/fs/cgroup/cgroup.controllers
|
|
639
|
-
# cpuset cpu io memory pids ← memory delegated, all limits work
|
|
640
|
-
# cpuset cpu io pids ← memory missing, --memory is dropped
|
|
641
|
-
```
|
|
602
|
+
// Start a long-running service container. ensureRunning compares this
|
|
603
|
+
// config against the live container and recreates on drift — no per-
|
|
604
|
+
// plugin hash file or remove() dance needed.
|
|
605
|
+
await containers.ensureRunning("my-service", {
|
|
606
|
+
image: "myorg/myimage",
|
|
607
|
+
tag: "latest",
|
|
608
|
+
signalkDataMount: "/data", // resolves to the SignalK data dir, regardless of deployment
|
|
609
|
+
signalkAccessiblePorts: [8080], // port 8080 in the container must be reachable by SignalK
|
|
610
|
+
env: { MY_VAR: "value" },
|
|
611
|
+
restart: "unless-stopped",
|
|
612
|
+
});
|
|
642
613
|
|
|
643
|
-
|
|
614
|
+
// Get the actual address to connect to (resolved after ensureRunning)
|
|
615
|
+
const addr = await containers.resolveContainerAddress("my-service", 8080);
|
|
616
|
+
if (!addr) throw new Error("Container address not available");
|
|
617
|
+
// bare-metal → "127.0.0.1:8080" (or "127.0.0.1:8081" if 8080 was taken)
|
|
618
|
+
// containerised → "sk-my-service:8080" (Docker DNS, no host port exposed)
|
|
619
|
+
const response = await fetch(`http://${addr}/status`);
|
|
644
620
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
[
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
621
|
+
// Run a one-shot job
|
|
622
|
+
const result = await containers.runJob({
|
|
623
|
+
image: "myorg/converter",
|
|
624
|
+
command: ["convert", "--input", "/in/data.csv"],
|
|
625
|
+
inputs: { "/in": "/host/path/input" },
|
|
626
|
+
outputs: { "/out": "/host/path/output" },
|
|
627
|
+
timeout: 120,
|
|
628
|
+
});
|
|
653
629
|
```
|
|
654
630
|
|
|
655
|
-
|
|
656
|
-
just restarting Signal K) recreates the managed container with the
|
|
657
|
-
memory cap actually applied. Verify with `podman inspect sk-<name>
|
|
658
|
-
--format '{{.HostConfig.Memory}}'` — a non-zero value confirms the cap
|
|
659
|
-
is in cgroup state, not just on the command line.
|
|
631
|
+
See [doc/plugin-developer-guide.md](doc/plugin-developer-guide.md) for the full integration guide with gotchas and patterns.
|
|
660
632
|
|
|
661
|
-
|
|
662
|
-
override the kernel's controller delegation.
|
|
633
|
+
## API
|
|
663
634
|
|
|
664
|
-
|
|
635
|
+
| Method | Description |
|
|
636
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
637
|
+
| `getRuntime()` | Returns `{ runtime, version, isRootless, socketPath, ... }` (a `ContainerRuntimeInfo`) or `null` |
|
|
638
|
+
| `whenReady()` | Resolves once runtime detection settles (success OR failure). Replaces the polling-loop pattern; check `getRuntime()` after the await |
|
|
639
|
+
| `pullImage(image, onProgress?)` | Pull a container image (auto-qualifies for Podman) |
|
|
640
|
+
| `imageExists(image)` | Check if image exists locally |
|
|
641
|
+
| `getImageDigest(imageOrContainer)` | Local image ID (sha256) for an image:tag or container |
|
|
642
|
+
| `ensureRunning(name, config, options?)` | Create and start container if not running; auto-recreates on config drift across `image`, `tag`, `command`, `networkMode`, `env`, `volumes`, `ports` |
|
|
643
|
+
| `recreate(name, config, options?)` | Force-recreate: remove (if present) + ensureRunning. Always replaces an existing container (running or stopped) — use this for "update now" / plugin-startup self-heal flows where correctness must not depend on drift detection (1.12.0+) |
|
|
644
|
+
| `start(name)` | Start a stopped container |
|
|
645
|
+
| `stop(name)` | Stop a running container |
|
|
646
|
+
| `remove(name)` | Stop and remove a container |
|
|
647
|
+
| `getState(name)` | Returns `running`, `stopped`, `missing`, or `no-runtime` |
|
|
648
|
+
| `runJob(config)` | Execute a one-shot container job |
|
|
649
|
+
| `cleanupOrphanedJobs(filter)` | Reap `sk-job-*` containers leaked by a previous server lifecycle, filtered by the caller's `ownerPluginId`. Idempotent — returns `{ reaped: OrphanJobInfo[] }` so the plugin can roll back any per-job state it had written |
|
|
650
|
+
| `getLogs(name, options?)` | One-shot fetch of the last N lines of a container's combined stdout+stderr log. `tail` defaults to 200, max 10000; `since` is unix-epoch seconds |
|
|
651
|
+
| `prune()` | Remove dangling images |
|
|
652
|
+
| `listContainers()` | List all `sk-` prefixed containers |
|
|
653
|
+
| `execInContainer(name, command)` | Run a command inside a running container |
|
|
654
|
+
| `ensureNetwork(name)` | Create a Podman/Docker network if it doesn't exist |
|
|
655
|
+
| `removeNetwork(name)` | Remove a network |
|
|
656
|
+
| `connectToNetwork(container, network)` | Add a container to a network (bridge mode only) |
|
|
657
|
+
| `disconnectFromNetwork(container, net)` | Remove a container from a network |
|
|
658
|
+
| `updates.register(reg)` | Register a container for update detection |
|
|
659
|
+
| `updates.unregister(pluginId)` | Stop tracking updates for a plugin |
|
|
660
|
+
| `updates.checkOne(pluginId)` | Force a fresh update check (or coalesce with in-flight) |
|
|
661
|
+
| `updates.checkAll()` | Force a fresh update check for every registered plugin; resolves to the array of results |
|
|
662
|
+
| `updates.getLastResult(pluginId)` | Cached last result, no network |
|
|
663
|
+
| `updates.sources` | Built-in version-source factories — `sources.githubReleases(repo, options?)` and `sources.dockerHubTags(image, options?)` — convenience for building an `UpdateRegistration.source` |
|
|
664
|
+
| `manifest.get(pluginId)` | Read the persisted manifest for one consumer plugin, or `null` if none. Writes happen automatically after successful `ensureRunning` calls — this is read-only |
|
|
665
|
+
| `manifest.list()` | Return every persisted manifest in the data directory. Order is unspecified |
|
|
666
|
+
| `manifest.getContainerHistory(containerName)` | Bounded history (max 20 entries) of digest changes for a specific container. Throws "Ambiguous container history" if more than one manifest references the same `containerName` — disambiguate via `manifest.get(pluginId)` |
|
|
667
|
+
| `updateResources(name, limits)` | Apply new resource limits live, fall back to recreate |
|
|
668
|
+
| `getResources(name)` | Currently effective limits (plugin defaults ⊕ user override) |
|
|
669
|
+
| `resolveSignalkDataMount()` | Resolve the volume name or host path that backs `app.getDataDirPath()` in the current deployment; returns `null` if the runtime is not yet initialised |
|
|
670
|
+
| `resolveHostPath(absPath)` | Translate an arbitrary absolute path into the `{ source, subPath }` pair the runtime needs to mount it; handles bare-metal, bind, and named-volume topologies |
|
|
671
|
+
| `resolveContainerAddress(name, port)` | Return the `host:port` string to reach `port` on a managed container from the SignalK process; call after `ensureRunning()` with `signalkAccessiblePorts` set |
|
|
672
|
+
| `doctor.imageRunsAsUser(image, user?)` | Probe whether `image` runs cleanly under the host-UID mapping signalk-container will emit (1.8.0+). Never throws — returns `{ ok, output, error? }` |
|
|
673
|
+
| `doctor.selfDeployment()` | Diagnose the Signal K deployment itself: socket resolution, daemon reachability, rootless/rootful detection, and (when containerised) self-container ID. Returns `{ status, remediation, ... }` — see `SelfDeploymentResult` in `src/types.ts` |
|
|
674
|
+
| `doctor.generateSetupSnippet(format?, result?)` | Generate a ready-to-paste compose fragment (`format: "compose"`, default) or `podman/docker run` command (`format: "run"`) tailored to the detected runtime — wires up the socket bind-mount. Pure templating over `SelfDeploymentResult`; includes a `dockerfile` note (no image change needed) and operator notes. |
|
|
665
675
|
|
|
666
|
-
|
|
667
|
-
likely earlier Pi OS releases) and the systemd `Delegate=memory` snippet
|
|
668
|
-
above **doesn't work** — `cat /sys/fs/cgroup/cgroup.controllers` still
|
|
669
|
-
shows `cpuset cpu io pids` after a reboot — the cause is one level
|
|
670
|
-
deeper. The Pi's GPU firmware injects `cgroup_disable=memory` into the
|
|
671
|
-
kernel boot cmdline, so the memory controller never reaches systemd.
|
|
676
|
+
## REST Endpoints
|
|
672
677
|
|
|
673
|
-
|
|
678
|
+
All mounted at `/plugins/signalk-container/api/`:
|
|
674
679
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
680
|
+
| Method | Path | Description |
|
|
681
|
+
| ------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
682
|
+
| GET | `/runtime` | Detected runtime info |
|
|
683
|
+
| GET | `/containers` | List managed containers |
|
|
684
|
+
| GET | `/containers/:name/state` | Container state |
|
|
685
|
+
| POST | `/containers/:name/start` | Start a stopped container |
|
|
686
|
+
| POST | `/containers/:name/stop` | Stop a running container |
|
|
687
|
+
| POST | `/containers/:name/remove` | Stop and remove a container |
|
|
688
|
+
| GET | `/containers/:name/logs?tail=N&since=ts` | Last N lines of the container's combined stdout+stderr log (one-shot). `tail` defaults 200, max 10000 |
|
|
689
|
+
| GET | `/containers/:name/logs/stream` | Server-Sent Events stream of live log lines. Closes when the container is removed or the client disconnects |
|
|
690
|
+
| POST | `/prune` | Prune dangling images |
|
|
691
|
+
| GET | `/updates` | List last update-check results |
|
|
692
|
+
| GET | `/updates/:pluginId` | Last update-check result for one plugin |
|
|
693
|
+
| POST | `/updates/:pluginId/check` | Force a fresh update check (HTTP 200 even when offline) |
|
|
694
|
+
| GET | `/containers/:name/resources` | Effective resource limits + user override |
|
|
695
|
+
| POST | `/containers/:name/resources` | Apply new resource limits (live or recreate). Body is a `ContainerResourceLimits` diff against the consumer plugin's default. |
|
|
696
|
+
| DELETE | `/containers/:name/resources` | Clear any user override and restore the consumer plugin's pristine default limits to the running container. |
|
|
697
|
+
| POST | `/doctor/image` | Probe whether an image runs cleanly under the live host-UID mapping. Body: `{ image, tag?, user? }`. Never 5xx for a failed probe — `{ ok: false, error }` is a successful response (1.8.0+). |
|
|
698
|
+
| GET | `/doctor/deployment` | Diagnose this Signal K deployment: socket resolution, daemon reachability, rootless/rootful detection, self-container ID cascade. Returns a `SelfDeploymentResult` with `status` and copy-pasteable `remediation` lines. |
|
|
699
|
+
| GET | `/doctor/snippet?format=compose\|run` | Generate a ready-to-paste compose fragment or shell command for setting up Signal K with this runtime. `text/plain` by default; pass `Accept: application/json` for the structured `SetupSnippetResult`. |
|
|
679
700
|
|
|
680
|
-
|
|
681
|
-
revert instructions: **[doc/cgroup-memory-on-raspberry-pi-os.md](doc/cgroup-memory-on-raspberry-pi-os.md)**.
|
|
701
|
+
## Configuration
|
|
682
702
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
703
|
+
| Setting | Default | Description |
|
|
704
|
+
| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
705
|
+
| Preferred runtime | `auto` | Auto-detect, or force `podman`/`docker` |
|
|
706
|
+
| Auto-prune images | `weekly` | `off`, `weekly`, or `monthly` |
|
|
707
|
+
| Max concurrent jobs | `2` | Limit parallel one-shot job executions |
|
|
708
|
+
| Update check interval | `24h` | How often to check for container image updates (e.g. `24h`, `12h`, `1h`). Min 1h. |
|
|
709
|
+
| Background update checks | `true` | Periodically check for updates in the background. Disable on metered connections — manual checks via the UI button still work. |
|
|
710
|
+
| Disable user-namespace remap | `false` | Suppress rootless-Podman `--userns=keep-id` on filesystems that cannot be id-mapped (ZFS, some encrypted FS). Secondary escape hatch only — the recommended primary fix is host-side `fuse-overlayfs` storage (see [ZFS host notes](#zfs-and-other-idmap-incompatible-filesystems)). |
|
|
711
|
+
| Container overrides | `{}` | Per-container resource limits (CPU, memory, PIDs). Field-level merged on top of consumer plugin defaults. See dev guide. |
|
|
686
712
|
|
|
687
|
-
###
|
|
713
|
+
### ZFS and other idmap-incompatible filesystems
|
|
688
714
|
|
|
689
|
-
|
|
690
|
-
`~/.config/containers/systemd/`) or a systemd unit with
|
|
691
|
-
`Restart=always`, the unit silently restarts the SK container within
|
|
692
|
-
`RestartSec` seconds of any stop — including operator-initiated
|
|
693
|
-
`podman stop`. This races with manually-started replacement containers
|
|
694
|
-
on the same port.
|
|
715
|
+
Rootless Podman uses `--userns=keep-id` so files written into bind mounts land owned by the Signal K host user. On filesystems the Linux kernel cannot id-map — ZFS is the canonical case, some encrypted filesystems behave the same way — this mapping either fails at container create or triggers a multi-minute per-file `chown` sweep across the image layers.
|
|
695
716
|
|
|
696
|
-
|
|
697
|
-
restart/recovery policy before stopping the container and re-enable it
|
|
698
|
-
afterward. With a `--user` Quadlet (substitute your actual unit name):
|
|
717
|
+
The doctor (`GET /plugins/signalk-container/api/doctor/deployment`) flags this proactively when Podman is rootless and the storage root sits on a known-hazard filesystem; the response's `containerStorage.advice` array carries the up-to-date remediation steps. Two fixes exist, in order of preference:
|
|
699
718
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
# … run your test container on port 3000 …
|
|
703
|
-
systemctl --user unmask <your-signalk-unit>.service # re-enable
|
|
704
|
-
systemctl --user start <your-signalk-unit>.service
|
|
705
|
-
```
|
|
719
|
+
1. **Switch the host's rootless Podman storage driver to `fuse-overlayfs`.** Recommended whenever possible. Avoids the chown sweep entirely while preserving correct bind-mount ownership for every image. See [Podman's storage configuration docs](https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md) for the exact `storage.conf` form and migration steps; modern Podman releases also document `fuse-overlayfs` in `man containers-storage.conf`.
|
|
720
|
+
2. **Enable the "Disable user-namespace remap" setting** in this plugin's config panel. Escape hatch for hosts that cannot switch storage drivers. Bind-mount ownership stays correct for root-by-default managed containers (questdb, grafana, mayara); non-root images give up host-caller ownership in exchange for being able to start at all.
|
|
706
721
|
|
|
707
|
-
|
|
708
|
-
visibility into systemd-managed lifecycles.
|
|
722
|
+
If the host runs a recent-enough kernel + Podman + ZFS combination that supports kernel-level idmapped mounts natively, neither workaround is required — the doctor advisory will fall silent on its own once the hazard heuristic no longer matches.
|
|
709
723
|
|
|
710
724
|
## License
|
|
711
725
|
|