knext

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.

shell
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/.bin

Always 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:

shell
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
PackageWhat it is forCanonical imports
@getknext/coreThe 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/libRuntime 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/dbThe 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:

shell
npx @getknext/core create my-app --runtime node --cache redis --yes

See 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.

next.config.ts — Next.js 16.2+
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:

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.

knext.config.ts
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.

shell
# 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:

shell
$ 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.

CommandWhat it doesWhat it writes
npx @getknext/core createScaffold a new knext-ready Next.js app.New files only — no cluster changes.
npx @getknext/core init-ciGenerate a deploy workflow and the scoped RBAC it needs.Two files — no cluster changes.
npx @getknext/core validateCheck knext.config.ts is complete and valid.Nothing — reads only your config file.
npx @getknext/core deployBuild → 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 buildThe build and asset-upload steps only, without deploying.With object storage configured, uploads static assets to your bucket; no cluster write.
npx @getknext/core doctorCluster-prereq preflight: CRD, operator, admission webhook, ingress class, Knative Serving.Nothing — read-only.
npx @getknext/core statusThe operator's honest Ready / Degraded / Reconciling / DatabaseReady conditions.Nothing — read-only.
npx @getknext/core rollbackPin serving traffic to a prior Knative Revision (or clear the pin).One merge-patch of spec.traffic on the NextApp CR.
npx @getknext/core cleanupRemove the app from the cluster.Deletes the NextApp CR; the operator removes what it owns.
npx @getknext/core db bindBind an existing Postgres Secret as DATABASE_URL.One merge-patch of spec.database.secretRef on the NextApp CR.
npx @getknext/core db migrateApply pending drizzle migrations against the writer, once.Your database only — no cluster write.
npx @getknext/core gcReap 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.
shell
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 only

preview and loadtest are not knext subcommands — they ship as directly runnable entries in the package. See Directly runnable entries.

On this page