Deployment and packaging
How to run a Cruft app in production: getting the single binary from npm or a source build, exit codes and process lifecycle, sealing dependencies against supply-chain attacks, a Docker pattern, and the lockfile and content-addressed store.
Deploying a Node application means shipping your code and provisioning a Node runtime for it to run on. Deploying a Cruft application means shipping one binary that is the runtime. This guide covers getting that binary into production, running your app on it, hardening the supply chain, and containerizing. It reflects cruft 0.0.10, and the production gaps are noted in place.
Getting the binary into an environment
Cruft ships as a single self-contained binary. There is no separate engine, no system JavaScript runtime to install alongside it, and no shared-library runtime dependency to provision. Two paths get it onto a machine.
npm package. The cruft package on the npm registry is a thin launcher that wraps prebuilt platform binaries. Installing it pulls in exactly one platform-specific optionalDependency matching your host, and the launcher (bin/cruft.js) execs that binary and forwards argv, stdio, and the exit code. There is no postinstall step and nothing is downloaded at install time beyond the one platform package npm selects.
npm install -g cruft # global CLI
# or, as a project dev tool:
npm install cruft
The platform packages available today:
| Package | Host |
|---|---|
@cruft/cli-linux-x64 | Linux x64 |
@cruft/cli-linux-arm64 | Linux arm64 |
@cruft/cli-darwin-x64 | macOS Intel |
@cruft/cli-darwin-arm64 | macOS Apple Silicon |
@cruft/cli-win32-x64 | Windows x64 |
If you install with optional dependencies disabled (--no-optional, --omit=optional), the launcher will not find its binary and exits with a clear error telling you to reinstall. On an unsupported platform it prints the supported list and fails rather than guessing. The engines field requires Node >=18 to run the launcher; that constrains the npm install mechanism, not Cruft's own execution.
Build from source. With a Rust toolchain:
cargo build --release --bin cruft -p cruft
# binary at target/release/cruft
Verify either path the same way:
$ cruft --version
cruft 0.0.10
Running in production
Run a program by handing Cruft the entry file:
cruft app.js # or app.mjs, app.ts, app.tsx
A long-running server stays alive as long as the event loop has work (an open listener, a pending timer). There is no daemon mode built in; supervise Cruft the way you would supervise any single binary, with systemd, a container orchestrator, or a process manager, and let that supervisor own restart policy.
Exit codes use the sysexits.h conventions for startup and usage errors, rather than Node's "everything is 1." This makes supervisor and CI logic more precise. Runtime failures (an uncaught throw or unhandled rejection) still exit 1, matching Node, so existing crash-restart logic keeps working. From the error reference:
| Code | Class | Meaning |
|---|---|---|
0 | success | clean exit |
N | program choice | process.exit(N) is propagated faithfully |
64 | EX_USAGE | bad or unknown CLI flag |
65 | EX_DATAERR | --check syntax failure; TypeScript strip refusal |
66 | EX_NOINPUT | entry file not found |
1 | runtime failure | uncaught exception, unhandled rejection, or install resolution failure (Node-style) |
Process lifecycle. Cruft honors process.on(...) for exit, beforeExit, uncaughtException, and unhandledRejection. A production handler that logs and flushes on an uncaught error works for both asynchronous and synchronous top-level throws, so a handler like this reliably runs on a crash:
process.on("uncaughtException", (e) => {
console.error(e.stack); // full stack: named frames + file:///line:col
process.exit(1);
});
The default crash output also prints the full stack trace with file and line, so an unhandled error tells you where it failed, not just what failed. See the debugging guide for the full treatment.
Supply-chain hardening
Cruft has capability controls that constrain the process from the outside. Two of them are production-ready today and worth turning on by default; see the security model for the full matrix.
--sealed-deps is the zero-config production control. It denies node_modules code its own I/O authority per-caller while your application code continues to run normally. A compromised transitive dependency that tries to read /etc/hostname or open a socket is blocked, while your app's own reads succeed. This is enforced today and is the single highest-impact flag for running untrusted npm dependencies in production.
cruft --sealed-deps app.js
--audit records every I/O capability use together with the module that caused it, which is the way to learn your app's real capability footprint before you tighten anything.
Two things worth knowing as you tighten:
- Full
--sealed(fs/net/env/exec/stdio) enforces today. It gates network access, filesystem access, and directory enumeration, and a stdio grant lets a sealed program that needs to print be authorized to.--sealed-depsremains the simpler default for the common "trust my code, distrust my dependencies" case; reach for full--sealedwhen you want to allowlist the whole process. - The TLS client validates peer identity. It checks the certificate chain to a trusted root and also verifies hostname/SAN and expiry, so outbound HTTPS to third parties over the public internet is defended against a man-in-the-middle. It has not been hardened against a hostile network at scale, so treat that as something to watch rather than something proven bulletproof.
Docker
Cruft's single binary makes for a small image. The minimal realistic pattern is to install the npm package (which pulls the matching Linux platform binary), copy your app, and set the entry command.
FROM node:22-slim
WORKDIR /app
# Install cruft (pulls @cruft/cli-linux-<arch> automatically).
RUN npm install -g cruft
# Bring in the app and its locked dependencies.
COPY package.json cruft-lock.json ./
RUN cruft install
COPY . .
CMD ["cruft", "app.js"]
If you would rather not carry a Node base image, copy a prebuilt or source-built cruft binary into a minimal base (debian:stable-slim or a distroless image) and COPY it to a directory on PATH; the binary is self-contained, so nothing else needs installing. Match the binary's architecture to the image.
There is also front-door "wrap" support: Cruft's npm-installed wrapper can supervise node/npm/npx and package scripts inside clean Linux Docker images across Node 20/22/24, then fully unwrap without leaving host contamination. That is aimed at CI and container scenarios where you want Cruft to sit in front of an existing Node toolchain; see the wrap reference. It is a supervision layer; it does not sandbox the Node backend.
Dependencies in production
Cruft has its own package manager built into the binary: registry resolution, a lockfile, and a content-addressed store (see the package manager page).
For reproducible production installs, commit cruft-lock.json. Every install writes it as the exact resolution keyed by integrity hash; a later install restores straight from the lockfile and materializes from the store rather than re-resolving the registry.
cruft install # writes/uses cruft-lock.json
The store ($CRUFT_STORE, default ~/.cruft/store) keeps one content-addressed copy per version and hard-links (or copies, in isolated mode) it into each project, so repeated installs across projects and CI runs are cheap.
Two gaps to flag before you rely on cruft install in a CI build:
- Lifecycle scripts are not run.
preinstall/install/postinstalldo not execute, and the failure is silent: install reports success while a package that compiles or downloads at install time (esbuild-class native binaries) materializes incomplete and breaks later. If your build depends on a postinstall step, do not assumecruft installperforms it. - Only root
dependenciesare read.devDependenciesandoptionalDependenciesin the root manifest are ignored, silently, with a "0 installed" summary. A CI job that installs then reaches for a test runner or bundler declared indevDependencieswill find it missing.
For these two cases, run the dependency install with npm (against a Node toolchain) and use Cruft to run the result.