The Docker workflow
runs tests, creates semantic releases, and publishes multi-platform images to
Docker Hub. Release builds publish
the full version, minor, major, and latest tags; main-branch builds also publish
edge.
The server has two deliberately separate endpoints:
http://0.0.0.0:8080 (FOUNDRY_PORT): browser/Foundry HTTP and WebSocket traffic, intended only for a browser-trusted reverse proxy.https://0.0.0.0:10443 (DEVICE_PORT): versioned binary restricted-CBOR keypad WSS at /device/v1 and authorized firmware downloads, using its own persistent self-signed TLS identity. Current keypads use explicit frame protocol v2; the server retains the bounded legacy-v1 codec needed to update deployed keypads.The reverse proxy remains outside this repository. Configure the Foundry module with its external reverse-proxy host, port and /ws path; it already constructs the appropriate external wss:// URL.
services:
mindflayer-server:
build: .
user: node
environment:
MINDFLAYER_DATA_DIR: /data
MINDFLAYER_FIRMWARE_DIR: /firmware
FOUNDRY_PORT: 8080
DEVICE_PORT: 10443
ports:
- "10443:10443"
expose:
- "8080"
volumes:
- mindflayer-data:/data
- ./firmware:/firmware:ro
volumes:
mindflayer-data:
The image runs as the unprivileged node user. Ensure bind-mounted directories are writable by that UID where required. The image contains neither PlatformIO nor any firmware signing private key.
Published container images support linux/amd64, linux/arm64, and linux/s390x. Node.js 24 does not provide the Alpine base image for 32-bit ARM, so linux/arm/v6 and linux/arm/v7 are not published.
The image defaults persistent server identity, credentials, and the verified firmware cache to /data, accepts optional manually managed firmware at /firmware, and declares /data as a volume. The read-only /firmware mount is unnecessary for automatic updates. Its Docker health check verifies both /healthz on the Foundry HTTP listener and /healthz on the device HTTPS listener.
On first start the server creates /data/tls/device-key.pem (mode 0600) and a self-signed /data/tls/device-cert.pem. The certificate can be recreated while retaining the key; the private key is the durable server identity and must be backed up with the data volume. Extract the pinned public key with:
openssl pkey -in /data/tls/device-key.pem -pubout -out device-public.pem
The host bundle tool exports this public key as DER/SPKI into each keypad’s serial provisioning envelope. Losing the private key changes server identity; every keypad then needs trusted serial reprovisioning. Corrupt or mismatched key/certificate state fails startup instead of silently changing identity.
Create a unique 256-bit device secret:
MINDFLAYER_DATA_DIR=/data npm run device:provision -- controller1
The command stores /data/devices.json with restrictive permissions. Create a mode-0600 ignored serial bundle without printing its secret fields:
MINDFLAYER_DATA_DIR=/data MINDFLAYER_WIFI_SSID='ssid' \
MINDFLAYER_WIFI_PASSWORD='password' MINDFLAYER_SERVER_HOST='10.42.0.1' \
npm run device:bundle -- controller1 provisioning/controller1.provisioning.bin
npm run device:serial-provision -- provisioning/controller1.provisioning.bin /dev/serial/by-path/...
The serial sender automatically performs the keypad’s double-reset recovery sequence through FTDI RTS. Recovery boot leaves the physically shared GPIO3/RXD0 out of NeoPixel DMA mode, sends the validated envelope, waits for acknowledgement, and requires no button press.
Serial runtime diagnostics default to disabled. Set MINDFLAYER_SERIAL_DEBUG=true while creating a bundle to enable them for that device; use false or omit the variable for silent normal operation. Provisioning acknowledgements remain available regardless of this setting.
Connected keypads automatically install newer stable verified releases by default, including releases discovered while they are already connected. Installation reboots the keypad and preserves provisioning. See firmware updates for verification, polling, opt-outs, and offline operation.
To pin a keypad instead, add rollout metadata to its entry in devices.json and
restart the server:
{
"secret": "<existing secret>",
"targetVersion": "1.2.3",
"allowDowngrade": false
}
Without a target, the newest verified stable release is selected unless the entry
has "autoUpdate": false. A target takes precedence over automatic selection and
must exist in the verified cache or manual repository. Equal versions mean no
update. Automatic downgrades are never offered; only an explicit target with
"allowDowngrade": true permits one. Authenticated devices receive short-lived,
device-bound opaque download grants in an HTTP Authorization: Bearer header.
The server downloads the keypad release archive from GitHub and verifies its RSA-2048/SHA-256 signature against the bundled production public key before caching or offering it. It never compiles or signs firmware and needs no signing private key. The keypad independently verifies the signature before installation.
For an explicit manual/offline rollout, copy a trusted signed binary into the
read-only firmware mount and create manifest.json following
firmware/manifest.example.json. Each release declares schema version, hardware
ID, semantic version, relative artifact path, byte size and SHA-256. Startup
rejects invalid versions, missing or outside files, traversal, and size/hash
mismatches. Unlike automatic imports, this operator-managed path checks repository
integrity, not the RSA signature; installation still requires the keypad’s
independent signature verification. Manual releases require an explicit device
target and are never automatically selected as the latest release.
After HMAC authentication, an unpinned registration receives restricted-CBOR
FIRMWARE_ACCEPTED for its reported version; a pinned device receives it only for
the available configured target. This is distinct from authentication: it allows
the temporary rBoot candidate’s complete health gate to promote it. Acceptance
precedes any newer update offer, so discovering another release cannot prevent
the returning candidate’s health handshake. The server never knows or controls
rBoot slot numbers.
The server is licensed under GPL-3.0-only. Third-party dependencies retain their own licenses. Published containers include the project license and credits and declare the GPL license in their OCI metadata.
The server’s published branches and tags were rewritten on 2026-09-10 to make the project license consistently GPLv3 throughout history. Existing clones should be replaced with fresh clones after preserving local work; do not merge the old history back into the repository.
npm ci
npm test
npm audit
npm start
Commits on main are released with semantic-release. Conventional Commit types determine the next version, update package.json, package-lock.json, and CHANGELOG.md, create the Git tag and GitHub release, and supply matching semantic Docker tags and OCI version metadata.