Upgrading knext
Upgrade the operator and CRD before the CLI, and what the deploy-time schema check does when you don't.
knext has two pieces that version independently: the operator (with its NextApp CRD) running
in your cluster, and the CLI (@getknext/core) running on your machine or in CI.
They are deliberately not lockstep — but when you upgrade them, the order matters.
Upgrade the operator and its CRD first, then the CLI.
Why the order matters
knext deploy describes your app as a NextApp custom resource and applies it to the cluster.
The CRD installed by the operator defines which fields that resource may contain.
A newer CLI can emit a field a older CRD has never heard of. knext applies with strict validation on purpose, so the cluster rejects that resource rather than silently accepting it and dropping the field. Silently dropping it would be worse: your deploy would report success while the setting you asked for simply vanished — a TLS setting, a database credential reference, a scaling bound.
So the failure you get from upgrading in the wrong order is loud and safe. It is still a failure.
The safe direction
An older CLI against a newer CRD is always fine. A newer CRD understands every field an older CLI knows how to emit. You do not need to upgrade both at once, and you do not need to keep them on matching versions — you only need to avoid the CLI running ahead of the cluster.
That is what makes "operator first" a rule you can follow incrementally: upgrade the cluster when it suits you, and upgrade CLIs afterwards at whatever pace your team and CI move.
What it looks like when the order is wrong
knext deploy stops before it changes anything and names the field:
PREFLIGHT FAILED: the NextApp CRD installed on this cluster does not know field(s) this
CLI emits, so the CR would be rejected (or, under a client that does not assert strict
validation, SILENTLY PRUNED):
- spec.database.roSecretRef
Upgrade order is load-bearing: upgrade the OPERATOR/CRD first, THEN the CLI.
Nothing was built, uploaded or applied — this ran before any side effect.
kubectl get crd nextapps.apps.kn-next.dev -o jsonpath='{.spec.versions[*].name}'
knext doctorThree things to note.
Nothing was uploaded. The check runs as the very first step that touches your cluster — before the build is pushed and before any assets are uploaded to object storage. A failed deploy leaves no half-written state behind and no orphaned files to clean up.
It names the field. You get the specific setting your cluster cannot store, not a generic validation error, so you can tell immediately whether the gap matters for this deploy.
For some fields it also tells you what the gap would have cost. If the missing field is one
where silently dropping it would weaken your setup rather than merely change it, the message says
so. spec.database.roSecretRef is the clearest example: without it the operator never injects a
read-only database URL, so reads that were meant for a replica fall back to the read-write
credential — and the app would still report healthy.
Checking before you deploy
knext doctor reports whether the cluster's CRD covers the fields your CLI emits, so you can
find a version gap without running a deploy at all:
npx @getknext/core doctorIf doctor cannot read your cluster's full schema — some restricted credentials cannot — it will
say so rather than guess. The deploy-time check does not depend on that permission: it works with
exactly the access a deploy already needs, so a restricted credential can still deploy safely.
Upgrading the operator
Apply the operator release for the version you want, then confirm the CRD is in place. Name the version rather than a moving address, so the same command is reproducible later and tells you what to roll back to (see Pin, upgrade and roll back the operator):
kubectl apply --server-side -f \
https://github.com/getknext-dev/knext/releases/download/operator-vX.Y.Z/install-vX.Y.Z.yaml
kubectl get crd nextapps.apps.kn-next.dev
knext doctor # the "Operator version" row confirms what is now runningOnce that reports the version you expect, upgrade the CLI:
npm install -D @getknext/core@latestWhich features need the 1.3 operator
A 1.3 CLI against an older operator keeps working for everything the older operator already understands. Two features add a field to the resource only when you use them, so they are the ones that need the newer operator:
| Feature | Field | When the CLI emits it |
|---|---|---|
| Write-free pods | spec.security.writeFree | Only when knext deploy builds the image in that run (not with --image or --skip-build). |
| Private apps | spec.networking | Only for an app configured as private. |
knext doctor reports a CRD that lacks only these as a warning naming the feature. If you do
use one, knext deploy refuses before building or uploading anything and names the field. Upgrade
the operator first, then the CLI, as above.
Next.js security upgrades
Apps on Next.js 16.4.0 or older must upgrade to 16.4.1 or later. Three published Next.js advisories affect every 16.x release up to and including 16.4.0:
| Advisory | Severity | What it allows |
|---|---|---|
| GHSA-2464-fhr4-w9xw | Critical | Unauthenticated remote code execution in the Image Optimization API when SVG files are used |
| GHSA-673q-mjm6-r5xr | Critical | Unauthenticated remote code execution in 'use cache' functions that return typed arrays |
| GHSA-p3m5-59mp-jv5r | High | Denial of service in the App Router through Server Actions |
knext serves images through Next.js's optimizer and backs 'use cache' with its shared cache, so
both remote code execution paths are reachable on a knext app that has not upgraded. There is no
16.3.x patch: the fix ships only on the 16.4.x line. New apps from knext create already pin
16.4.3.
To upgrade an existing app, pin next to 16.4.3 (or any later 16.4.x) in its package.json,
reinstall so the lockfile resolves it, then rebuild and redeploy — the running image keeps the old
Next.js until you deploy a new one:
npm install next@16.4.3 # or: bun add next@16.4.3 / pnpm add next@16.4.3
knext doctor # the Next.js advisory row should now PASS
knext deployknext doctor reads the Next.js your app actually resolves and reports FAIL (exit code 1),
naming each advisory, while it is still on an affected version, so you can gate CI on it. Apps on
the 15.x line are affected by the image optimizer and Server Actions advisories up to 15.5.27;
the fix there is 15.5.28 — although knext itself supports Next.js 16 only.
Moving to 16.4 also moves the app from the Next.js minor the official compatibility suite has
credentialed (16.3) to one that is supported but not yet credentialed; knext doctor says so as a
warning. That is the deliberate trade: a known remote code execution is worse than a not-yet-finished
credential window. See the compatibility matrix for where the credential
stands.
Version-specific step: upgrading a 0.4.x app to 1.0
1.0 changes five things a 0.4.x app needs to walk through, in this order.
1. Upgrade the operator and CRD first
Same rule as above, restated for this specific jump: apply the 1.0 operator release before installing the 1.0 CLI anywhere that deploys to this cluster.
kubectl apply -f https://github.com/getknext-dev/knext/releases/download/operator-latest/install.yaml
kubectl get crd nextapps.apps.kn-next.devAn older CLI (0.4.x) continues to work against the upgraded operator — you do not need to touch every app's tooling in the same step. Only installing the 1.0 CLI before the operator is upgraded risks the rejected-field failure described above.
2. Rename your config file to knext.config.ts
The config file is now knext.config.ts. If your app's config still has the filename knext used
before 1.0, there is no dual-read and no warning-and-continue: running any knext command in that
directory fails fast with one actionable error naming the exact rename to make. Run the rename it
tells you to, then run the command again — nothing renames an existing app's file for you.
knext create scaffolds knext.config.ts for new apps.
3. Pin your build target explicitly if you build with vinext
The default build target changed from vinext (the compiled single executable) to turbopack
(next build → the standalone runtime image), and the default runtime for that shape changed from
node to bun (the compiled bytecode executable). If your knext.config.ts (after the rename
above) omits build, your next deploy tries to build and ship a different artifact than before.
If your app builds with vinext (its build script runs vite build), add build: 'vinext' to
your config before upgrading, or knext build/deploy looks for a .next/standalone tree your
build script never produces and fails:
const config = {
name: 'acme',
registry: 'registry.example.com/acme',
build: 'vinext',
};
export default config;Apps already on the standalone target (build: 'turbopack'/'webpack', or building with next build) are unaffected by the build-target change. If those apps also left runtime unset, it now
defaults to bun (compiled bytecode) instead of node — set runtime: 'node' explicitly if you
want the previous, uncompiled behaviour.
4. Know about the new request-body cap
Every route on a deployed app now rejects a request body larger than 8 MiB by default, on every
build target and runtime — a 413 Payload Too Large, with the connection closed after the rest of
the body is discarded (never buffered) for at most two seconds. A chunked request with no
Content-Length is capped the same way.
If your app accepts uploads larger than 8 MiB through a route handler, raise the cap before your
users hit it — set KNEXT_MAX_REQUEST_BYTES (bytes) in your config's env map or in spec.env on
the NextApp. 0 removes the cap entirely (the app logs this loudly on start so an unintentional
0 is visible). The app also prints the cap actually in force on start:
REQUEST_BYTE_CAP:<bytes> (<source>).
5. Check whether your app writes to /tmp or .next/cache at runtime
Standalone apps no longer get a writable /tmp or .next/cache by default; the container's root
filesystem is read-only. If your own code writes to os.tmpdir(), or anywhere else at runtime that
isn't your database or object storage, that write now fails. Before upgrading a running cluster, set
spec.security.writableCache: true on the NextApp (this field needs the 1.0 operator from step 1,
and an older operator rejects it with a strict-decoding error). Two shapes keep their previous
behaviour with nothing to change: apps with spec.storage configured keep a writable
.next/cache, and single-executable builds (build: 'vinext' or selfContained: true) keep a
writable /tmp. See Read-only root filesystem.
Version-specific step: the metrics port moved from 9091 to 9464
Older releases served app metrics on :9091 — a port Knative's queue-proxy binds inside every pod
on a stock Serving install, which could crash-loop apps with EADDRINUSE. Current releases serve
metrics on :9464 everywhere: the runtime's default, the prometheus.io/port annotation, the
default NetworkPolicy's scrape grants, the shipped PodMonitor and the bundled Grafana dashboards
all move together when you upgrade the operator and redeploy your apps.
What does not move automatically is anything you wrote yourself against the old port:
- Hand-rolled Prometheus
scrape_configs, or ServiceMonitors/PodMonitors you authored — repoint them from9091to9464. - Grafana panels or alert rules of your own pinned to the old target.
- Any NetworkPolicy you added alongside knext's default one that grants
9091for scraping. - Redeploy each app after upgrading: a pod still running an image built before the change
binds
:9091until its next deploy, and during that window the upgraded operator's scrape configuration points at:9464— so that app's metrics are dark until it is rebuilt.
Verify an app after redeploying (from a pod or node that can reach it):
kubectl exec deploy/<your-scraper> -- wget -qO- http://<pod-ip>:9464/metrics | head -3knext doctor (the metrics-port check) flags any app still pinned onto a queue-proxy-owned
port and tells you which serving ConfigMap key governs :9091 on your cluster.
Version-specific step: the build id now comes from KNEXT_BUILD_ID
knext deploy used to hand the deploy tag to next build as NEXT_DEPLOYMENT_ID. From Next.js
16.2.11, a deployment id makes next build ignore generateBuildId and write the same fixed build
id on every deploy, so standalone (turbopack or webpack) deploys stopped at the build-id check.
The CLI now sets KNEXT_BUILD_ID instead, and leaves NEXT_DEPLOYMENT_ID unset for standalone
builds.
Update generateBuildId in your next.config to read the new variable first, and remove any
deploymentId line from a standalone app's config:
generateBuildId: () => process.env.KNEXT_BUILD_ID || process.env.NEXT_DEPLOYMENT_ID || null,Until you do, a standalone deploy stops before anything is uploaded or applied, with
Update generateBuildId in next.config to read KNEXT_BUILD_ID. vinext apps keep working without
the change, but the same line is correct for them too. See Skew protection.
The scale-to-zero database: roll the failover controller before its new config
The scale-to-zero database (Beta) ships its own failover controller — a small component that watches the primary pageserver and promotes the warm standby if it dies. It reads its behaviour from environment configuration set on its manifests, and it follows the same ordering rule as the operator and CLI, for the same reason: the piece that reads a contract upgrades before the manifest that writes it.
When an upgrade adds or changes a failover-controller setting — the tenant set it promotes, the routed pageserver URL it reads generations from, the maintenance-freeze ConfigMap name or the freeze's hard time bound — roll the failover-controller image to the new version first, then apply the manifests that wire the new configuration. Apply the new setting against the old image and the setting is inert: the running controller does not know the variable exists, so nothing reads it and the change silently does nothing — the same failure mode as running the CLI ahead of the CRD.
If you deploy the scale-to-zero database from its bundled manifests
(kubectl apply -f packages/scale-zero-pg/deploy/), bump the controller image digest and roll it
before applying the rest of the directory in the same upgrade.
What this does not cover
Deployment tools that apply manifests directly to your cluster — GitOps controllers, for example —
do not go through knext deploy, so they do not get the preflight check described above. If you
apply NextApp resources through such a tool, keep the same upgrade order yourself: the cluster is
still the thing that decides which fields it can store.