Node.js Single Executable Applications (SEA)
Bundle your Node.js application into a single executable binary with no runtime dependency. Covers the SEA workflow and production tips.
What you'll learn
- ✓What single executable applications are
- ✓How to create a SEA from a Node.js project
- ✓Bundling assets and handling dependencies
- ✓Cross-compilation and distribution strategies
Prerequisites
- •Basic Node.js knowledge
- •Node.js 20 or later installed
- •Familiarity with npm and the build process
Node.js Single Executable Applications (SEA) let you distribute your application as a standalone binary. The end user does not need Node.js installed. You ship one file, they run it. This is useful for CLI tools, internal utilities, and edge deployments where installing a runtime is impractical.
How it works
SEA works by injecting a JavaScript blob into a copy of the Node.js binary itself. When the modified binary runs, it executes your bundled code instead of starting the usual Node.js REPL. The result is a single file that contains both the runtime and your application.
The process has three steps:
- Create a preparation blob from your JavaScript source
- Copy the Node.js binary
- Inject the blob into the copied binary
Step-by-step: creating your first SEA
1. Write your application
// app.js
const args = process.argv.slice(2);
const command = args[0];
if (command === 'greet') {
const name = args[1] || 'World';
console.log(`Hello, ${name}!`);
} else if (command === 'version') {
console.log('my-cli v1.0.0');
} else {
console.log('Usage: my-cli <greet|version> [name]');
}
2. Create a SEA configuration file
{
"main": "app.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true,
"useSnapshot": false,
"useCodeCache": true
}
Save this as sea-config.json. The useCodeCache option pre-compiles your JavaScript to V8 bytecode, which speeds up startup.
3. Generate the preparation blob
node --experimental-sea-config sea-config.json
This produces sea-prep.blob, a binary file containing your compiled application.
4. Copy and inject
On macOS:
cp $(command -v node) my-cli
codesign --remove-signature my-cli
npx postject my-cli NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
--macho-segment-name NODE_SEA
codesign --sign - my-cli
On Linux:
cp $(command -v node) my-cli
npx postject my-cli NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
On Windows:
copy node.exe my-cli.exe
npx postject my-cli.exe NODE_SEA_BLOB sea-prep.blob `
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
5. Run it
./my-cli greet Alice
# Hello, Alice!
./my-cli version
# my-cli v1.0.0
The binary is self-contained. Copy it to any machine with the same OS and architecture, and it runs without Node.js installed.
Bundling a real project
Most applications have dependencies. Since SEA takes a single JavaScript file as input, you need to bundle your project first. Use esbuild for this:
npm install --save-dev esbuild
{
"scripts": {
"bundle": "esbuild src/index.js --bundle --platform=node --outfile=dist/app.js --external:fsevents",
"build-sea": "npm run bundle && node --experimental-sea-config sea-config.json"
}
}
Update sea-config.json to point to the bundled output:
{
"main": "dist/app.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true,
"useCodeCache": true
}
Handling native modules
Native addons (.node files) cannot be bundled into the blob. If your project uses native modules like better-sqlite3 or sharp, you need to distribute them alongside the binary or find pure JavaScript alternatives.
Mark native modules as external in esbuild:
esbuild src/index.js --bundle --platform=node \
--outfile=dist/app.js \
--external:better-sqlite3 \
--external:sharp
Then ship the native module files alongside your binary.
Including assets
You can embed static assets (templates, config files, etc.) into the blob using the SEA assets feature:
{
"main": "dist/app.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true,
"useCodeCache": true,
"assets": {
"template.html": "assets/template.html",
"config.json": "assets/default-config.json"
}
}
Access embedded assets in your code:
import { getAsset } from 'node:sea';
const template = getAsset('template.html', 'utf-8');
const config = JSON.parse(getAsset('config.json', 'utf-8'));
This eliminates the need to distribute separate asset files alongside your binary.
V8 startup snapshot
For faster cold starts, you can use a V8 startup snapshot. This pre-initializes your application’s state and bakes it into the binary:
{
"main": "dist/app.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true,
"useSnapshot": true
}
With snapshots enabled, the V8 engine restores the heap from the snapshot instead of parsing and executing your JavaScript from scratch. This can reduce startup time significantly for large applications.
Note: snapshot mode has restrictions. You cannot use top-level import(), and some APIs behave differently during snapshot creation.
Automating the build
Here is a complete build script:
// build-sea.js
import { execSync } from 'node:child_process';
import { cpSync, chmodSync } from 'node:fs';
import { platform } from 'node:os';
const APP_NAME = 'my-cli';
const NODE_PATH = process.execPath;
// Step 1: Bundle
console.log('Bundling...');
execSync('npx esbuild src/index.js --bundle --platform=node --outfile=dist/app.js', {
stdio: 'inherit',
});
// Step 2: Generate blob
console.log('Generating SEA blob...');
execSync('node --experimental-sea-config sea-config.json', {
stdio: 'inherit',
});
// Step 3: Copy node binary
const binaryName = platform() === 'win32' ? `${APP_NAME}.exe` : APP_NAME;
cpSync(NODE_PATH, binaryName);
// Step 4: Remove signature on macOS
if (platform() === 'darwin') {
execSync(`codesign --remove-signature ${binaryName}`);
}
// Step 5: Inject blob
console.log('Injecting blob...');
const machoFlag = platform() === 'darwin' ? '--macho-segment-name NODE_SEA' : '';
execSync(
`npx postject ${binaryName} NODE_SEA_BLOB sea-prep.blob ` +
`--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 ${machoFlag}`,
{ stdio: 'inherit' }
);
// Step 6: Re-sign on macOS
if (platform() === 'darwin') {
execSync(`codesign --sign - ${binaryName}`);
}
// Step 7: Make executable
if (platform() !== 'win32') {
chmodSync(binaryName, 0o755);
}
console.log(`Built: ./${binaryName}`);
Size considerations
The resulting binary includes the entire Node.js runtime, so expect a minimum size of about 80-100 MB. To reduce this:
- Use
useCodeCache: trueto skip parsing at runtime (slightly larger binary, faster startup) - Strip debug symbols:
strip my-clion Linux - Compress with UPX:
upx --best my-cli(reduces size by 50-70% but slightly slower startup) - Consider alternatives like Bun or Deno if binary size is critical
When to use SEA
Good use cases:
- CLI tools: Distribute to users who may not have Node.js installed
- Internal utilities: Ops tools that need to run on any server
- Edge computing: Deploy to environments with limited package management
- Kiosk applications: Single-file deployment to embedded systems
Not ideal for:
- Web servers: Docker containers are a better deployment model
- Frequently updated apps: Rebuilding the binary for every change is slow
- Projects with many native addons: The bundling complexity outweighs the benefits
Key takeaways
Node.js SEA turns your JavaScript application into a standalone binary that runs without a Node.js installation. Use esbuild to bundle your project into a single file, generate a SEA blob, and inject it into a copy of the Node.js binary. Embed assets directly using the assets configuration. The binary is portable across machines with the same OS and architecture. For CLI tools and utilities, SEA simplifies distribution dramatically.
Related articles
- Backend Graceful Shutdown: Finishing Work Before Exiting
Implement graceful shutdown in backend services. Covers signal handling, draining connections, health check coordination, and timeout strategies.
- Node.js Node.js vs Deno vs Bun: JavaScript Runtimes Compared
Compare Node.js, Deno, and Bun on performance, security, compatibility, and developer experience. Choose the right JavaScript runtime for 2026.
- Node.js Node.js Cluster Module for Multi-Core Scaling
Scale your Node.js application across all CPU cores using the cluster module. Covers load balancing, zero-downtime restarts, and production patterns.
- Node.js Corepack: Managing Package Managers in Node.js
Use Corepack to enforce consistent package manager versions across your team. Covers setup, configuration, and CI integration.