Getting started
Take a Next.js app from next build to a scale-to-zero Knative service.
Take an existing Next.js app from next build to a scale-to-zero Knative service. knext's default
build uses Next.js's own official Deployment Adapter API — a plain next build, wired through
adapterPath. A selectable vinext target — an open-source, Vite-based implementation of
Next.js, compiled into a single executable — is also available; see the adapter and the
compiled build for the trade-off.
Prerequisites. A Kubernetes cluster with Knative Serving installed (don't have one?
Your first cluster gets you a laptop cluster in minutes), Next.js 16.x, and a
container registry. An object-storage bucket (gcs, s3, minio, or azure) is optional —
start without one (your app serves its static files from its own image, like next start) and add
it later for CDN offload. See
Starting without object storage.
0 · Install the packages
knext ships on the public npm registry under the @getknext scope. No registry
configuration, no auth token — a plain npm i is all it takes.
npm i @getknext/core # the adapter, the config type, and the CLI (bin: knext)
npx @getknext/core --version # the CLI is on your PATH via node_modules/.binAlways run the CLI as npx @getknext/core ... (or a locally installed
knext, after npm i @getknext/core), as shown throughout this page.
Never run bare npx knext on its own — the unscoped name knext on
the public npm registry belongs to an unrelated package, so outside a
project where @getknext/core is already installed, npx knext downloads
and runs someone else's code instead of this CLI.
The CLI command was renamed from kn-next to knext. kn-next still works
as a deprecated alias — same command, same flags — and it does not print
the reminder on every invocation shape, so do not assume its absence means
you already switched. A locally installed kn-next run directly (on your
PATH, or from an npm run script naming it) prints a one-line reminder to
switch; npx/npm exec (including npx @getknext/core ...) does NOT,
since npm's own bin-resolution for that path is ambiguous between the two
names; pnpm's dlx/exec and Windows .cmd/.ps1 shims don't either.
kn-next will be removed in 2.0. Update scripts/CI to knext before then.
Two optional packages complete the picture — add them only if you need them:
npm i @getknext/db # typed drizzle-orm data layer over your DATABASE_URL
npm i @getknext/lib # runtime helpers: pooled DB clients, health checks, logger| Package | What it is for | Canonical imports |
|---|---|---|
@getknext/core | The official Next.js adapter, the typed deploy config, the ISR cache handler, and the knext CLI. Required. | '@getknext/core' (KnativeNextConfig), '@getknext/core/adapter', '@getknext/core/validate' |
@getknext/lib | Runtime helpers for an app running on knext: pooled Postgres clients, a MinIO client, a Cerbos client, health checks, and a preconfigured logger. | '@getknext/lib/clients', '@getknext/lib/health', '@getknext/lib/logger', '@getknext/lib/context' |
@getknext/db | The Data SDK — a thin drizzle-orm layer over your DATABASE_URL, with schema builders and one-shot writer-only migrations. | '@getknext/db', '@getknext/db/schema', '@getknext/db/migrate' |
What each package exports. @getknext/core exports the KnativeNextConfig type from its root,
the adapter from /adapter (what adapterPath points at), the ISR cache handler from
/adapters/cache-handler, and the pure config validator (validateConfig,
ConfigValidationError) from /validate. @getknext/lib exports getDbPool() / getDbPoolRO()
/ closeDbPool(), getMinioClient(), getCerbosClient(), checkDeepHealth() /
checkShallowHealth(), the logger, and the request-correlation helpers — all also re-exported
from the package root. @getknext/db exports getDb() / getDbRO() plus the whole of
drizzle-orm.
Starting a new app instead? npx @getknext/core create my-app scaffolds one with all of the
steps below already done. On a terminal it asks five questions — runtime (bun or node),
builder (turbopack, webpack, or vinext), cache (none or redis), object storage (none,
gcs, s3, minio, or azure), and React Compiler (on by default; --no-react-compiler
leaves it off). Enter takes the default each time. Every question has a flag, and --yes skips
them all, so CI never waits:
npx @getknext/core create my-app --runtime node --cache redis --yesSee knext create for what each answer writes.
You do not need to run the npm i above first: npx @getknext/core create my-app works from
any directory. If you did install into a parent directory, that is fine too — the scaffolded
next.config.ts pins Next.js's file-tracing root to the app itself
(outputFileTracingRoot), so a package-lock.json in the parent does not move the root.
1 · Point Next.js at the adapter
Reference the knext adapter from your next.config.ts. The adapter defaults output to 'standalone'
at build time. If you set output: 'export' yourself, the adapter leaves it alone and the knext CLI
refuses the build on both the Next.js and vinext targets: a static export is not a server deployment, and knext deploys a Next.js server.
Remove output: 'export', or host the out/ folder on a static host.
import type { NextConfig } from 'next';
const config: NextConfig = {
// knext ships the official NextAdapter as a package export.
// adapterPath is top-level config on Next.js 16.2+.
adapterPath: '@getknext/core/adapter',
};
export default config;Next.js 16.0.x–16.1.x: the option lives under experimental on those versions
(experimental: { adapterPath: '@getknext/core/adapter' }). Next.js 16.2+ auto-migrates the old
experimental key with a warning; 16.0.x does not recognize the top-level form.
An app below a lockfile. Next.js picks its file-tracing root by walking up to the outermost
lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock) or pnpm-workspace.yaml.
When that is a directory above your app, it writes the standalone server to
.next/standalone/<your-app>/server.js instead of .next/standalone/server.js, and
knext build stops with an error that names the inferred root and the lockfile responsible.
output: 'standalone' is not the problem. Fix it by pinning the root in next.config.ts:
const config: NextConfig = {
adapterPath: '@getknext/core/adapter',
output: 'standalone',
// This app's own directory.
outputFileTracingRoot: import.meta.dirname,
turbopack: { root: import.meta.dirname },
};or remove the lockfile you do not need. knext deploy reads outputFileTracingRoot too, to
choose the directory it sends to docker build.
A workspace monorepo is the one case where a root above the app is what you want. If the app shares packages with a root above it, set that root on purpose in both keys and knext packages the nested layout; see Deploy from a workspace monorepo. A root that was only inferred from a parent lockfile is never treated that way.
2 · Write the deploy config
knext reads a typed knext.config.ts. The config type is KnativeNextConfig, exported from
@getknext/core. The minimum valid config requires just name and registry. The storage
block is optional: without it, static assets are served from your app's container image — every
deploy says so explicitly — and you can add the block later for bucket/CDN delivery.
import type { KnativeNextConfig } from '@getknext/core';
const config: KnativeNextConfig = {
name: 'acme',
registry: 'registry.example.com/acme',
// Optional — omit to serve static assets from the image (like `next start`):
storage: {
provider: 'gcs', // one of: gcs | s3 | minio | azure
bucket: 'acme-assets',
publicUrl: 'https://storage.googleapis.com/acme-assets',
},
scaling: { minScale: 0, maxScale: 20 }, // minScale 0 = scale to zero
// The default build compiles your standalone server into a Bun bytecode
// executable. See /docs/bun-runtime. Set runtime: 'node' to run it under Node.
};
export default config;Bun is the default runtime and the recommended choice for most apps; Node is fully supported too
— see Which runtime should I use? before you set
runtime.
If you configure storage, its provider is validated against gcs | s3 | minio | azure. Each
shells out to that cloud's CLI (gsutil / aws / mc / az), which must be installed and
authenticated in your build environment. See Multi-cloud deploy.
The scaling block also carries the full set of autoscaling knobs (containerConcurrency,
poolMax, warmSchedule, burst-response tuning) — see Scale to zero.
To bind an existing Postgres, add a database block — see
Databases.
3 · Build, push, deploy
deploy is the default command — build → push → apply the NextApp CR. Validation runs
automatically when the config loads.
# build + push (digest is resolved automatically) and apply the NextApp CR
$ knext deploy --registry registry.example.com/acme
✓ image registry.example.com/acme@sha256:9f1c...
✓ NextApp/acme applied
✓ url https://acme.apps.example.com (scaled to 0)knext deploy flags: -r/--registry, -b/--bucket, -t/--tag, -n/--namespace,
--context, --image, --skip-build, --skip-upload, --skip-image-lockstep-check,
--dry-run (print the CR without applying), -h/--help, -v/--version. See the
CLI reference for what each does.
Then watch the rollout to real readiness — status exits non-zero if the app fails to come up, so
this pair works as a CI gate:
$ knext status acme --watch # polls until Ready=True (bounded)The operator is the source of truth. The CLI only ever applies the NextApp CR. The Go
operator reconciles it into a Knative Service — wiring autoscaling, networking, and the
runtime environment, and rejecting any image that is not digest-pinned. See
Operator & the NextApp CRD.
The whole CLI at a glance
Every command below is npx @getknext/core <verb>. With no command, or with flags only, the CLI runs
the default deploy flow. An unrecognised first word is an error, not a deploy. Full flag tables
are on the CLI reference.
| Command | What it does | What it writes |
|---|---|---|
npx @getknext/core create | Scaffold a new knext-ready Next.js app. | New files only — no cluster changes. |
npx @getknext/core init-ci | Generate a deploy workflow and the scoped RBAC it needs. | Two files — no cluster changes. |
npx @getknext/core validate | Check knext.config.ts is complete and valid. | Nothing — reads only your config file. |
npx @getknext/core deploy | Build → push → apply the digest-pinned NextApp CR. The default. | Applies the NextApp CR; with object storage configured, also uploads static assets to your bucket. |
npx @getknext/core build | The build and asset-upload steps only, without deploying. | With object storage configured, uploads static assets to your bucket; no cluster write. |
npx @getknext/core doctor | Cluster-prereq preflight: CRD, operator, admission webhook, ingress class, Knative Serving. | Nothing — read-only. |
npx @getknext/core status | The operator's honest Ready / Degraded / Reconciling / DatabaseReady conditions. | Nothing — read-only. |
npx @getknext/core rollback | Pin serving traffic to a prior Knative Revision (or clear the pin). | One merge-patch of spec.traffic on the NextApp CR. |
npx @getknext/core cleanup | Remove the app from the cluster. | Deletes the NextApp CR; the operator removes what it owns. |
npx @getknext/core db bind | Bind an existing Postgres Secret as DATABASE_URL. | One merge-patch of spec.database.secretRef on the NextApp CR. |
npx @getknext/core db migrate | Apply pending drizzle migrations against the writer, once. | Your database only — no cluster write. |
npx @getknext/core gc | Reap superseded _next/static/<build-id>/ prefixes (over-keep, never over-delete). | With object storage configured, deletes old asset prefixes from your bucket (reads the CR only); with none, reports there is nothing to reap. |
npx @getknext/core doctor # before your first deploy
npx @getknext/core deploy -r registry.example.com/acme # build → push → apply
npx @getknext/core status acme --watch # poll until Ready=True
npx @getknext/core rollback acme --to acme-00002 # pin 100% to a prior revision
npx @getknext/core db bind acme --secret acme-db # DATABASE_URL from a Secret
npx @getknext/core db migrate --dir ./drizzle # writer-only, idempotent
npx @getknext/core gc --dry-run # print the reap/keep plan onlypreview and loadtest are not knext subcommands — they ship as directly runnable entries
in the package. See Directly runnable entries.