SerialPilot

/03 — Recipes

Docker

Containers don't see USB devices unless you let them. Pass the device through with --device, fingerprint with udev rules, and find by VID/PID — paths are not your friend.

A minimal Dockerfile

FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "index.js"]

Nothing serial-specific here — the binding compiles itself during npm install. The node:20 base already has the C++ toolchain.

Run with a device

$ docker run --device /dev/ttyUSB0 \
-e SERIAL_PORT=/dev/ttyUSB0 \
my-app

The --device flag is the magic. Without it, your app sees an empty /dev and SerialPilot.list() returns nothing.

Multiple devices

$ docker run \
--device /dev/ttyUSB0 \
--device /dev/ttyACM0 \
-e SERIAL_PORT=/dev/ttyUSB0 \
my-app

Repeat the flag for each device. There's no glob form — explicit beats implicit when only some tty* devices belong to your app.

udev rules for stable names

Linux assigns device paths in plug order, so /dev/ttyUSB0 can change between reboots. Pin a stable symlink with udev:

# /etc/udev/rules.d/99-serial.rules
SUBSYSTEM=="tty", ATTRS{idVendor}=="2341", ATTRS{idProduct}=="0043", SYMLINK+="arduino"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="ftdi"

Reload udev (sudo udevadm control --reload + replug), and your container can now --device /dev/arduino.

Or skip paths entirely

Inside the container, find by VID/PID:

const [info] = await SerialPilot.findPorts({ vendorId: '2341' })
const port = new SerialPilot({ path: info.path, baudRate: 115200 })

Pair this with --device-cgroup-rule on the host to permit the container to see any device matching a class — useful when devices come and go.

docker-compose

services:
app:
build: .
devices:
- /dev/ttyUSB0:/dev/ttyUSB0
environment:
SERIAL_PORT: /dev/ttyUSB0
restart: unless-stopped

Gotchas

  • Permissions: if the host has the device owned by dialout, the container's UID needs that GID too. Easiest fix: --group-add dialout on docker run.
  • Hot-plug: --device is evaluated at start time — devices added later aren't visible. Restart the container after a replug, or mount the parent device class with cgroup rules.
  • Multi-arch builds: if you cross-build for ARM (Raspberry Pi etc), make sure npm install runs inside the target image, not on your build host. The native binding has to match the runtime arch.
Don't --privileged It works, but it grants every device and capability. Stick with explicit --device + --group-add — narrower blast radius and the same outcome.

Edit this page on GitHub