How your app is built
knext's default build compiles your Next.js app into a single self-contained executable through vinext — vite build, a nitro Bun bundle, then a bytecode-precompiled Bun binary.
When you run kn-next build, knext turns your Next.js app into one self-contained executable
rather than a folder of JavaScript to be interpreted at boot. That binary — the server, every route
it reaches, and the Bun runtime, compiled together — is what your image runs directly. This page
explains the build target, the pipeline that produces it, and what your app has to satisfy to build
on it.
For the performance numbers and a plain description of the artifact, see the compiled executable. For how this relates to the official Next.js adapter, see the adapter and the compiled build.
The build target
knext offers three build targets, and you choose between them with the build field.
The default target compiles your app with vinext, an open-source, Vite-based implementation of the Next.js framework, into a single executable. You do not have to configure anything to get it:
const config: KnativeNextConfig = {
name: 'acme',
registry: 'registry.example.com/acme',
// build: 'vinext' is the default — you can state it explicitly, but you don't need to
};The second target is next build's standalone output, run by a small supervisor that spawns
Next's own server.js and drains it gracefully on shutdown. The official Next.js compatibility
suite number was earned on this target's standalone output; the supervisor knext wraps it in has
not itself been run through the suite. Select it with build: 'turbopack', and pick which runtime
executes it with runtime:
const config: KnativeNextConfig = {
name: 'acme',
registry: 'registry.example.com/acme',
build: 'turbopack', // next build -> .next/standalone, run by a supervisor
runtime: 'bun', // or 'node' (the default runtime for this target)
};The third target is the same standalone output, produced with webpack instead of Turbopack as the
bundler. Select it with build: 'webpack'; everything else about it — the entry, the supervisor,
the runtime choice, the bytecode caching — is identical to the turbopack target, because both
are next build with a different bundler flag, and the standalone output they produce is the same
shape:
const config: KnativeNextConfig = {
name: 'acme',
registry: 'registry.example.com/acme',
build: 'webpack', // next build --webpack -> .next/standalone, run by a supervisor
runtime: 'bun', // or 'node' (the default runtime for this target)
};Both the turbopack and webpack targets have a prerequisite in your project, not just in the
config: kn-next build runs your app's own build script, so that script must run next build
(with --webpack for the webpack target) and output: 'standalone' set in next.config.ts —
otherwise there is no standalone server for the image to copy. Apps created by kn-next create are
vinext apps whose build script runs vite build and whose next.config.ts deliberately omits
output, so switching such an app to either of these targets means changing both first.
If you select build: 'turbopack' or build: 'webpack' and your build script still doesn't
produce .next/standalone (for example, the build script wasn't updated), kn-next build fails
immediately with an actionable error telling you to set output: 'standalone' in
next.config.ts and make sure your build script runs next build. This is a hard failure on
purpose: without it, the first sign of the missing artifact used to be an opaque error deep
inside the Docker image build at deploy time.
kn-next stages and builds the matching standalone runtime image for you. On the vinext target,
runtime: 'node' switches from the Bun executable to a Node server — see
vinext on Node below. On the standalone targets (turbopack and webpack,
identically) it decides how the server is packaged:
runtime: 'bun'—kn-next buildcompiles the standaloneserver.jsinto a Bun single executable with bytecode, verifies the bytecode is actually there (the build fails otherwise), and the image runs that executable inside the standalone folder. Needs Bun 1.4 or newer where you build. Details on the Bun runtime page.runtime: 'node'— the supervisor spawnsserver.jsunder Node, and bytecode caching comes from the V8 compile cache in the image (see bytecode caching).
vinext on Node
The vinext target also runs on Node. Set runtime: 'node' and keep build at its default:
const config: KnativeNextConfig = {
name: 'acme',
registry: 'registry.example.com/acme',
build: 'vinext', // the default
runtime: 'node', // a Node server instead of the Bun executable
};Nothing is compiled into an executable on this target. Instead:
- The build emits a Node server.
vite buildproduces nitro's Node preset rather than the Bun one, withknext-node-entry.mjsas the server entry. That entry provides what the Bun entry does: the health route, the:9464metrics endpoint, and draining in-flight requests onSIGTERM.vite.config.tspicks the preset from theruntimeinkn-next.config.ts, sokn-next build,kn-next deployand a plainnpm run buildall agree. - Bytecode caching is the V8 compile cache, baked into the image. The image
(
Dockerfile.vinext-node, next to yourDockerfile) boots the server once duringdocker buildand warms your configured health check path (healthCheckPathinkn-next.config.ts, default/api/health) before shipping the compiled code as a layer.kn-next deployandkn-next preview— the two commands that actually rundocker build— thread a customhealthCheckPaththrough automatically as a--build-arg, so a custom health route works with nothing extra to configure through either of them.kn-next builddoes not: it only stagesDockerfile.vinext-nodeinto your app (and never runs docker itself), so if you build the staged Dockerfile by hand, pass--build-arg KNEXT_HEALTH_CHECK_PATH=<your path>yourself. Every pod starts with the cache, including the first one after scaling to zero. The image build fails if the warm-up request does not succeed or the cache comes out too small, rather than shipping an empty cache — see diagnosing a failed bake. To see the cache being used, start a container withNODE_DEBUG_NATIVE=COMPILE_CACHE: the log says the cache for/app/.output/server/index.mjswas accepted.
ADockerfile.vinext-nodestaged before this fix (noARG KNEXT_HEALTH_CHECK_PATH) needs re-staging if your app uses a customhealthCheckPath: delete the file and re-runkn-next build, which never overwrites an existing one. - Bun is not needed to build it. Only the Bun executable target requires Bun 1.4 on the build machine.
kn-next buildchecks the preset. If the build produced the Bun preset (see below), it stops before uploading anything and tells you what to change. Running a Bun-preset server under Node would crash on startup.
Apps created before this target existed need two changes. kn-next build adds
Dockerfile.vinext-node and its ignore file to your app if they are missing, and it never
overwrites your own copies. It does not edit your vite.config.ts, which may still hardcode
preset: 'bun'. Make the nitro plugin follow the runtime:
import knext from './kn-next.config';
const onNode = knext.runtime === 'node';
// inside plugins: [...]
nitro({
preset: onNode ? 'node' : 'bun',
entry: onNode ? './knext-node-entry.mjs' : './knext-bun-entry.mjs',
rollupConfig: { output: { inlineDynamicImports: true } },
}),Then copy knext-node-entry.mjs from a freshly created app into yours, and declare the server
package it imports in your package.json: "srvx": "0.11.22". The entry drains in-flight
requests and waits for after() work before exiting on SIGTERM.
Two differences from the Bun executable, stated plainly:
- Image optimization serves originals unless sharp can load. The Node image does not ship
sharp's native addon.
/_next/imagethen serves the source image unoptimized and logs one warning. It never fails a request. - The request body cap is enforced as the body is read, not before your handler runs. The limit
is the same
KNEXT_MAX_REQUEST_BYTES.
The pipeline
kn-next build produces the executable in two stages:
vite build— vinext compiles your app and emits a nitro server bundle targeting the Bun preset (a.outputdirectory whose entry is an ES module).bun build --compile --minify --bytecode— that bundle is minified, precompiled to bytecode ahead of time, and baked together with the Bun runtime into one binary, cross-built for the Linux target your image runs (musl by default, matching the shipped base image).
Compiling the whole bundle at once is what lets the executable win both cold start and throughput at the same time: minification and bundling remove the per-module boundary cost that made earlier file-by-file bytecode approaches trade one for the other. The engine never parses your server source at boot — that work already happened at build time.
The native image-optimization codec cannot live inside the binary, so knext ships it beside
the executable in the image and loads it by path; /_next/image serves optimized AVIF/WebP exactly
as it does on a folder deployment. See image optimization.
Some server dependencies stay outside the nitro bundle and are loaded when the server runs. For
example, vinext keeps every @opentelemetry/* package your app depends on external. On a folder
deployment those load from .output/server/node_modules, which does not exist next to the
executable, so kn-next build bakes them into the binary. If one of them cannot be resolved at
build time, the build still succeeds but prints a warning:
[knext compile] WARNING: the entry runtime-requires package(s) that do not resolve from … and
cannot be bundled: <package> — the binary throws if that code path runsInstall the named package as a dependency of your app and rebuild. Otherwise, any request that
reaches code using that package fails with Cannot find module '<package>'.
Requirements
Your app must be an ES module
vinext builds through Vite, whose server/client (RSC ↔ SSR) module graph requires your app to be an
ES module. Set it in package.json:
{
"type": "module"
}kn-next create sets this for you, so a freshly scaffolded app already satisfies it. It matters only
when you point the compiled build at an existing app that was written as CommonJS. In that case:
- add
"type": "module"topackage.json; - name your Next.js config
next.config.mjsornext.config.cjs(a barenext.config.jsis interpreted as ESM once"type": "module"is set); - if the project still has CommonJS files that rely on
require()or on a.jsextension meaning CommonJS, rename them to.cjs(or convert them toimport/export).
Without "type": "module", the build fails while wiring up React Server Components — the
server/client module graph cannot be resolved — rather than producing a broken binary. A CommonJS
app is not supported on this target.
Building requires Bun 1.4 or newer
The compile step shells out to bun build --compile, so the machine doing the build needs
Bun 1.4.0 or newer on its PATH. (The kn-next CLI itself runs under plain Node; only this one
step needs Bun.) The floor is enforced, not advisory: an older Bun cannot serve the compiled tree
correctly, so the build refuses to run under it rather than quietly producing a binary that boots
roughly twice as slowly. Upgrade with bun upgrade. If Bun is missing entirely the build stops and
tells you how to install it.
CI and container builds get Bun the same way; the requirement is only on whoever runs the build, not on anyone deploying an already-built image.
Compatibility, stated honestly
knext's official-suite credential — 778/778, re-verified nightly — was earned
on the standalone target (the build: 'turbopack' shape above), which is a different artifact
from the compiled vinext executable. If suite-verified compatibility is what you need, that target
is selectable today — with one caveat stated plainly: the suite runs against the standalone server
directly, so the credential covers that output rather than the supervisor knext boots it with. The
suite lane for the compiled vinext executable is being stood up; until it
publishes a green run, treat the compiled build's compatibility as measured per feature rather
than suite-verified, and read the per-feature status on the
compatibility matrix rather than assuming the standalone number carries over.
The remaining gaps are largely a function of how far the underlying vinext framework has matured on
areas such as streaming/Suspense flushing, Partial Prerendering, Cache Components ('use cache'),
edge middleware, and internationalized routing. The matrix marks each honestly — ✅ only where a
red-on-fail check backs it, ⚠️ where something is implemented but unguarded, and ❌/⛔ where it is a
known or upstream-gated gap. Nothing there is papered over, and the first published suite number for
the compiled build may be low — it gets published anyway.
Which page answers what. This page covers how the build works and what it needs. For the measured cold-start and throughput numbers see the compiled executable; for what is and isn't verified see verified compatibility and the compatibility matrix.