Ship a Node.js app as a single executable binary
What if you could hand someone a single file that runs your Node.js program — no node to install, no npm install, nothing? That's what Single Executable Applications (SEA) do: Node bundles your script together with the runtime into one native binary. It's still an experimental feature, but it works today. Every step below was run on a stock node:26-slim Docker image, and the finished binary was tested on a plain Debian image with no Node installed.
The app
A small CLI. Note two SEA-specific touches: node:sea's isSea() tells you when you're running inside the bundle, and getAsset() reads files you embedded at build time. The argument parsing uses the built-in util.parseArgs, so there are zero dependencies.
// hello.js
const { parseArgs } = require('node:util');
const { isSea, getAsset } = require('node:sea');
const { values } = parseArgs({
options: { name: { type: 'string', short: 'n', default: 'world' } },
});
console.log(`Hello, ${values.name}!`);
console.log('Running inside a single executable?', isSea());
if (isSea()) {
console.log('bundled asset:', getAsset('banner.txt', 'utf8').trim());
}The build config
A small JSON file tells Node what to bundle. The assets map embeds arbitrary files into the binary — here a banner.txt we read back with getAsset().
{
"main": "hello.js",
"output": "sea-prep.blob",
"assets": {
"banner.txt": "banner.txt"
}
}Building the executable
Four steps: generate a preparation blob, copy the node binary under your app's name, inject the blob into that copy with postject, and run it.
# 1. generate the blob from the config
$ node --experimental-sea-config sea-config.json
Wrote single executable preparation blob to sea-prep.blob
# 2. copy the node binary, named after your app
$ cp "$(command -v node)" hello
# 3. inject the blob into the copy
$ npx postject hello NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
# 4. run it
$ ./hello --name codewithnodejs
Hello, codewithnodejs!
Running inside a single executable? true
bundled asset: built with Node.js Single Executable ApplicationsProof: it runs with no Node.js installed
The whole point is a runtime-free target. Here's the exact binary running inside a bare debian:bookworm-slim image that has no Node at all — it only needs one shared library (libatomic1):
$ docker run --rm -v "$PWD":/w -w /w debian:bookworm-slim sh -c '
apt-get install -y libatomic1 # the one extra lib the binary needs
which node || echo "no node installed"
./hello --name debian'
no node installed
Hello, debian!
Running inside a single executable? true
bundled asset: built with Node.js Single Executable ApplicationsThe catches
- It's big. The binary is ~142 MB, because it embeds the entire Node.js runtime.
- Still experimental. Running it prints
ExperimentalWarning: Single executable application is an experimental feature. - Not fully static. The binary is dynamically linked — the target machine still needs the C runtime (
glibc) andlibatomic. That's why the Debian run installslibatomic1. - Per-platform. You're copying this machine's Node binary, so build on the OS and CPU architecture you're targeting — a Linux build won't run on macOS or Windows.
- CommonJS entry. The
mainscript is evaluated as CommonJS; if your app has imports or dependencies, bundle it to a single file first (esbuild, rollup) and pointmainat the output. - macOS & Windows need extra steps to remove and re-apply code signing after injection.
When it's worth it
SEA shines for distributing a CLI to people who don't have (or don't want) Node, and for dropping a program onto a machine with no runtime installed. It is not a size optimization — for containers, a slim or distroless image is far smaller. Reach for it when "one file, double-click, run" is the goal.