Node and Docker
I recommend the Node target for production. It runs B4.run's complete HTTP runtime, so it's the one to choose when you need Agent Protocol, AG-UI, middleware, filesystem-backed capabilities or an execution sandbox.
PrerequisitesCopy link to section: Prerequisites
- Use Node 24 or newer. B4.run packages require it.
- Keep
@b4run/cliin productiondependencies, because the emitted server imports it at runtime. - Run
b4 buildon the host before you build an image. The generated Dockerfile copies.b4/buildinto the image and runs the server from there. - Put authentication, tenant authorization and network restriction around the full service before you expose it.
For an npm-managed app, make the CLI a production dependency like this:
npm install @b4run/cli --saveSelect the Node targetCopy link to section: Select the Node target
B4.run emits Node and LangSmith when build.targets is absent. To build only Node, list it:
import { config } from "@b4run/cli"
export default config({
build: { targets: ["node"] },
})Emitted filesCopy link to section: Emitted files
b4 build writes:
.b4/build/modules.mjsholds static imports for the discovered routes, tools, state, route memory and middleware..b4/build/server.mjsloads that manifest and callsserveRuntime()on the production listener.Dockerfileis a marker-managednode:24-slimimage definition.
If the root Dockerfile still carries B4.run's generated marker, the next build refreshes it. If it has no marker, B4.run leaves it alone and writes the generated version to .b4/build/Dockerfile.
Run directlyCopy link to section: Run directly
Use b4 start to run the same Node assembly without Docker:
b4 build
b4 startThe default listener is 0.0.0.0:8000. Override it with --host and --port, or with HOST and PORT:
b4 start --host 127.0.0.1 --port 3000Use the generated DockerfileCopy link to section: Use the generated Dockerfile
Create .dockerignore before the first docker build, or merge these exclusions into your existing file. Keep .b4/build in the context, because the generated image runs its server.mjs:
.env
.env.*
.git
.github
node_modules
**/node_modules
coverage
*.log
.DS_StoreNow emit a fresh .b4/build on the host and build the image:
b4 check
b4 build
docker build -t my-b4-app .
docker run --rm -p 127.0.0.1:8000:8000 --env-file .env my-b4-appThe loopback-only publish (127.0.0.1:8000:8000) is fine for a local smoke test. In production, put the service behind an authenticated proxy or network policy.
The generated Dockerfile uses COPY . ., so every unignored file in the build context can end up in an image layer. That includes a secret that a later Dockerfile instruction deletes. docker run --env-file .env injects runtime values, but it does not undo a secret already baked into an image layer. Review the final context and image history in CI. Pass production secrets through the runtime or orchestrator, and keep them out of the build context.
The generated image runs npm ci --omit=dev || npm install --omit=dev. Its copy step tolerates an optional pnpm lockfile, but the install always uses npm. If your production install needs pnpm, Yarn or stricter lockfile behavior, write your own Dockerfile.
The image's command is node .b4/build/server.mjs, so don't exclude .b4/build in .dockerignore.
Environment and secretsCopy link to section: Environment and secrets
Pass model credentials, database URLs and provider configuration at runtime. For local Docker smoke tests, --env-file .env is convenient. In production, use the orchestrator's secret mechanism, and keep secret files out of the image.
src/middleware.ts is execution middleware. It isn't service-wide authentication: health, thread management and state routes, cancellation and memory-candidate management include paths that bypass it. Your outer authentication and tenant checks must cover the whole service, exempting only probes you mean to make public.
Filesystem and sandboxCopy link to section: Filesystem and sandbox
The Node runtime can use local .b4 stores and filesystem-backed capabilities, and it can build the configured execution sandbox. Each of these persists separately:
.b4inside a container stays local to that container, unless the deployment mounts durable storage there or swaps in shared stores.- Sandbox workspaces follow the selected sandbox provider's volume lifecycle.
- Shared Postgres checkpoints, threads and permissions leave long-term memory and workspace files local. Those need their own shared stores.
Choose those stores in Persistence and Tenancy, and coordinate replicas as described in Production Topology.
Health and shutdownCopy link to section: Health and shutdown
GET /healthz returns a process-level success response. Treat it as liveness, not dependency readiness. The handler never queries the model, database or sandbox provider. In an embedded fetch/Hono assembly with requestStores, stores are built before route dispatch, so even a health request can fail before the handler runs. That still isn't a complete dependency check.
The generated .b4/build/server.mjs does not currently install signal handlers. serveRuntime() handles signals only when a caller opts in with installSignalHandlers: true. A larger host can handle signals itself and call await runtime.close() in its shutdown path. Plan rollout grace periods and request draining around this.
Production checklistCopy link to section: Production checklist
- Use Node 24+ and keep
@b4run/cliindependencies. - Create or audit
.dockerignore, then runb4 check,b4 buildandb4 verifybefore building the image. - Test the built image on loopback, including Agent Protocol and AG-UI streaming.
- Inject secrets at runtime, and authenticate and restrict the whole service.
- Configure durable stores, backups and retention for every kind of state you rely on.
- Keep one replica unless thread-aware routing or distributed serialization and cancel routing are guaranteed.
- Account for the generated server's missing signal handlers during rollout.
TroubleshootingCopy link to section: Troubleshooting
- The container can't import
@b4run/cli: move it fromdevDependenciestodependencies, rebuild on the host, then rebuild the image. server.mjsis missing in the image: runb4 buildfirst and remove.b4/buildfrom.dockerignore.- Your custom Dockerfile wasn't replaced: B4.run keeps unmarked root files. Look in
.b4/build/Dockerfilefor the generated one. - The health probe is green but requests fail: test the actual database, model, sandbox, authentication and route path.
/healthzchecks none of them. - Cancellation or the one-run gate behaves inconsistently across replicas: route each thread to the process running it, or add distributed serialization and cancel routing.