Node.js workloads¶
How it works¶
Brewlet delivers your JavaScript separately from the Node.js runtime. Your OCI
application image contains code, resolved package files and an explicit launch
descriptor, but not the Node executable. The platform approves and provisions
a Node.js runtime image, and your Kubernetes Deployment
selects it by runtime ID. Brewlet runs the declared command with your
application files mounted read-only under /app.
Dependencies can ship inside the application image or be published separately as a dependency layer set or governed bundle. Brewlet does not run npm, discover a package entrypoint or rewrite module resolution.
An update starts new Pods; it never replaces modules inside a running Node.js process. See concepts and the launch descriptor for the execution model. The runtime root is also read-only, so temporary files and caches need explicit writable volumes.
Tested runtime images¶
Brewlet has been tested with Node.js 22 using:
- unchanged distroless images (
gcr.io/distroless/nodejs22-debian13), where the executable is/nodejs/bin/nodeand is not on the defaultPATH; - Docker Official
node22 images adapted for Brewlet.
The exact digests are listed in the upstream and adapted test matrices. They are test inputs, not a security recommendation.
The Node.js example fixture runs daily against main in the
Node.js example workflow. It does not gate pull
requests or releases; Python is the only qualification fixture (see
verification).
Always use the runtime ID, executable path and platforms supplied by your
platform team. node-22 below is an example ID; Brewlet does not infer a
Node.js version from it.
Prepare the application files¶
This example uses two ES modules. src/main.mjs imports
../dependencies/greeting.mjs, so the two layers must keep that relative
layout under /app.
-
Create the directories:
mkdir -p node-demo/src node-demo/dependencies cd node-demo -
Save the dependency as
dependencies/greeting.mjs:export function message() { return 'shared-contract'; } -
Save the application as
src/main.mjs:import http from 'node:http'; import { message } from '../dependencies/greeting.mjs'; function report() { return { language: 'node', runtime: process.versions.node, dependency: message(), arguments: process.argv.slice(2), uid: process.getuid(), gid: process.getgid(), }; } if (process.argv.includes('--probe')) { console.log(JSON.stringify(report())); } else { http.createServer((_request, response) => { response.writeHead(200, { 'Content-Type': 'application/json' }); response.end(JSON.stringify(report())); }).listen(8000, '0.0.0.0'); } -
Make the files world-readable and the directories searchable:
chmod 0755 src dependencies chmod 0644 src/main.mjs dependencies/greeting.mjs
This example needs no npm install. For a real npm project, install from your
reviewed lockfile before packaging and package the resulting node_modules
layout. Native addons and install-time builds must target the selected Linux
ABI, Node ABI and CPU architecture.
Write the launch descriptor¶
Save as launch.json:
{
"schemaVersion": 1,
"process": {
"entrypoint": [
"/nodejs/bin/node"
],
"args": [
"/app/src/main.mjs"
],
"workingDir": "/app"
}
}
The import is explicit and relative, so NODE_PATH is not needed. Do not use
NODE_PATH as a general ES-module resolution mechanism: a CommonJS project may
use its documented resolution configuration, while an ESM package needs the
correct package/import layout or an explicit build output.
Build and push¶
Run on Linux or macOS. On macOS, follow the producer filesystem requirements. Replace the registry with your own:
brewlet build registry.example.com/apps/node-demo:v1 \
--platform linux/arm64 \
--launch launch.json \
--layer "dependency=./dependencies:/app/dependencies" \
--layer "app=./src:/app/src"
brewlet push registry.example.com/apps/node-demo:v1 --push-result push.json
This produces separate dependency and app layers. push.json records the
pushed digest. To consume an independently published dependency set instead,
use dependency layer sets.
Deploy¶
Replace the image digest, runtime ID, platform and namespace with values from your platform team. Resource values are examples.
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-demo
namespace: apps
spec:
replicas: 1
selector:
matchLabels: {app: node-demo}
template:
metadata:
labels: {app: node-demo}
annotations:
brewlet.sh/runtime-image: node-22
spec:
runtimeClassName: brewlet
nodeSelector: {kubernetes.io/arch: arm64}
containers:
- name: app
image: registry.example.com/apps/node-demo@sha256:<application-digest>
securityContext: {readOnlyRootFilesystem: true}
resources:
requests: {cpu: 100m, memory: 64Mi}
limits: {cpu: "1", memory: 256Mi}
readinessProbe:
httpGet: {path: /, port: 8000}
Registry credentials, non-root defaults and volumes work as described in
Package and deploy. The application
itself binds 0.0.0.0:8000; an OCI ExposedPorts entry does not configure
that.
Verify¶
-
Wait for the rollout and run the probe through
kubectl exec:kubectl -n apps rollout status deployment/node-demo --timeout=5m kubectl -n apps exec deployment/node-demo -- /nodejs/bin/node \ /app/src/main.mjs --probe "two words"The output includes
"arguments":["--probe","two words"]and a numeric, non-rootuid. -
Forward the port and query the service from another terminal:
kubectl -n apps port-forward deployment/node-demo 18080:8000 curl --fail http://127.0.0.1:18080/Expect
"language":"node", the actual Node.js version and"dependency":"shared-contract". Stop the port-forward afterwards. -
Check the actual runtime binding to confirm which runtime image the Pod uses.
Pitfalls and limits¶
package.jsonscripts are not used to find the entrypoint. Write the executable and complete arguments explicitly; no shell is assumed.- Listing several platforms does not make
.nodeaddons or OS-specific dependencies portable. Build separate platform payloads when needed. /appis read-only. Put application data and caches on explicit volumes. Node.js options are set by the application, not injected by Brewlet.- Updating the runtime image does not fix vulnerabilities in your own npm dependencies; rebuild or recompose the application for that.