3 · Configure the deploy
The health route that everyone forgets, and the config file that describes the deployment.
Two files. One of them is the single most common reason a first deploy sits at Ready=False.
The health route — do not skip this
export const dynamic = 'force-dynamic';
export function GET() {
return Response.json({ status: 'ok' });
}Why it matters more than it looks. The operator points Knative's readiness and liveness probes
at /api/health by default. create-next-app does not generate that route.
knext create writes this file for you. If you scaffolded the app with knext create, the
route above is already there — skip ahead. This section is for an existing app you are adding knext
to, which is where the missing route bites.
Without it, the probe gets a 404. The revision never becomes Ready. Your deploy "succeeds", the pod
starts, and then knext status reports Ready=False forever — with nothing obviously wrong in
your application logs, because nothing is wrong with your application. It is being asked a
question it cannot answer.
That is enough to go Ready. For a real app you want a health check that reports on its dependencies
too — @getknext/lib ships checkShallowHealth() and checkDeepHealth() for that.
Keep the probe route shallow. It should answer "is this process serving?", never "is every
dependency awake?". knext scales databases and caches to zero, so an asleep dependency is the normal
state — a probe that dials one flaps readiness on every cold wake, and liveness then restarts a pod
that was perfectly healthy. Put dependency checks on a separate path (/api/health/deep by
convention) and point alerting at that, not Kubernetes.
Prefer a different path? Set healthCheckPath in the config below and the operator points the probes
there instead.
Describe the deployment
Create knext.config.ts beside next.config.ts:
import type { KnativeNextConfig } from '@getknext/core';
const config: KnativeNextConfig = {
name: 'acme',
registry: 'registry.example.com/acme',
storage: {
provider: 'gcs',
bucket: 'acme-assets',
publicUrl: 'https://storage.googleapis.com/acme-assets',
},
scaling: { minScale: 0, maxScale: 10 },
};
export default config;Only two things are required: name and registry. The storage block is optional — without it,
your app serves its static files from its own image. This example configures a bucket; if you do,
provider, bucket and publicUrl are all required.
Substitute your own registry, bucket and publicUrl. The values above are placeholders and will
fail — deliberately, rather than appearing to work against something that is not yours.
Private registries
Most registries need credentials to pull from — private GHCR, OCIR, ECR and private Docker Hub all do. Name the Kubernetes Secret that holds them:
imagePullSecrets: ['acme-registry'],Create that Secret once, in the namespace you deploy into:
kubectl create secret docker-registry acme-registry \
--docker-server=registry.example.com \
--docker-username='<user>' \
--docker-password='<token>'knext writes it onto the app's service account, so every revision — and the node-local image
pre-pull, if you enable it — pulls with the same credentials. Leave it out and a private image
leaves the first pods stuck in ImagePullBackOff; adding the secret afterwards does not rescue the
running revision, so you would have to deploy again. Naming it here is the whole fix.
Env vars and secrets — .env.production is not how production gets them
A local .env/.env.production file behaves differently for the two kinds of value Next.js reads
from it, and neither behavior means "put secrets there":
-
NEXT_PUBLIC_*values are inlined into the client JavaScript bundle at build time. They ship to every browser that loads the app and are readable by anyone — never put a secret behind aNEXT_PUBLIC_prefix, in.env.productionor anywhere else. -
Plain (non-
NEXT_PUBLIC_) values are server-side only, and knext's runtime image never carries a baked.env.productionfor the server to read them from at boot. If you are used to Next reading server-side config from that file at runtime, stop relying on it here — deploy it and the value is simply absent. Move it toknext.config.tsinstead: -
Plain, non-secret values —
envinknext.config.ts, applied straight to the container. -
Secrets — a Kubernetes Secret, referenced from
secrets.envFrom/secrets.envMapinknext.config.ts. The operator injects it at the container level; it is never written to a file inside the image.
The one line that matters most
scaling: { minScale: 0, maxScale: 10 }minScale: 0 is what makes this a scale-to-zero deployment. Change it to 1 and you have an
ordinary always-on service — same app, same everything else, completely different cost and latency
character.
It is worth knowing that it is a single knob, because it means the choice is reversible. If cold starts turn out to matter more than idle cost for ACME, you change one number and redeploy.
maxScale: 10 caps how far Knative will fan out under load. It is a spend guard as much as a
capacity setting.
What you have now
Two config files and a route handler. Nothing has touched a cluster yet — and if you do not have one ready, this is a good place to pause and set that up (Install).