A complete guide to deploying Next.js 16 to Kubernetes in production, including multi-stage Dockerfile, K3s deployment manifests, health checks, HPA, Cloudflare Tunnel integration, environment variables, and Prisma in containers.
Introduction
Next.js has become one of the most popular frameworks for building full-stack web applications. But deploying it to Kubernetes in production requires careful attention to container optimization, health checks, graceful shutdown, and caching. This guide covers everything we need to go from a Next.js 16 project to a production-ready Kubernetes deployment on K3s.
We assume familiarity with Next.js basics and basic Kubernetes concepts. By the end, we will have a complete Dockerfile, Kubernetes manifests, Horizontal Pod Autoscaler, and Cloudflare Tunnel integration.
1. Configuring Next.js for Standalone Output
The standalone output mode is essential for containerized deployments. It bundles only the files needed to run the application, drastically reducing image size.
In next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;
2. Multi-Stage Dockerfile
This Dockerfile produces a minimal production image:
# Stage 1: Install dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
# If using Prisma, copy the schema before install
COPY prisma ./prisma/
RUN npm ci --ignore-scripts
# Generate Prisma client if needed
RUN npx prisma generate
# Stage 2: Build the application
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build-time environment variables
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
RUN npm run build
# Stage 3: Production image
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy the standalone output
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
# If using Prisma, copy the generated client
COPY --from=deps /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=deps /app/node_modules/@prisma ./node_modules/@prisma
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Graceful shutdown handler
CMD ["node", "server.js"]
Key points:
- Three stages keep the final image small (typically 150-200 MB vs 1 GB+ with a naive Dockerfile).
- Non-root user for security.
- Static files copied separately because the standalone output does not include them.
- Prisma client must be explicitly copied to the runner stage.
3. Handling Environment Variables
Next.js has two types of environment variables:
NEXT_PUBLIC_*: Inlined at build time into the client bundle. Must be available duringnpm run build.- Server-side variables: Available at runtime via
process.env.
For Kubernetes, we pass build-time variables as Docker build args and runtime variables through the Kubernetes Deployment:
# Build with public env vars
docker build \
--build-arg NEXT_PUBLIC_API_URL=https://api.example.com \
-t myapp:v1.0.0 .
4. Kubernetes Deployment Manifests
Namespace
apiVersion: v1
kind: Namespace
metadata:
name: myapp
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: myapp
spec:
replicas: 3
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
terminationGracePeriodSeconds: 30
containers:
- name: myapp
image: registry.example.com/myapp:v1.0.0
ports:
- containerPort: 3000
name: http
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url
- name: REDIS_URL
value: "redis://redis-svc.myapp:6379"
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
Service
apiVersion: v1
kind: Service
metadata:
name: myapp
namespace: myapp
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 3000
name: http
type: ClusterIP
Health Check Endpoint
Create a simple API route for health checks:
// app/api/health/route.ts
import { NextResponse } from "next/server";
export async function GET() {
// Add database connectivity check if needed
return NextResponse.json({ status: "ok" }, { status: 200 });
}
5. Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp
namespace: myapp
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 3
maxReplicas: 15
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
6. Cloudflare Tunnel Integration
Instead of exposing the cluster with a cloud load balancer, we use Cloudflare Tunnel:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: myapp
spec:
replicas: 2
selector:
matchLabels:
app: cloudflared
template:
metadata:
labels:
app: cloudflared
spec:
containers:
- name: cloudflared
image: cloudflare/cloudflared:latest
args:
- tunnel
- --no-autoupdate
- run
- --token
- $(TUNNEL_TOKEN)
env:
- name: TUNNEL_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-tunnel
key: token
Configure the tunnel to route traffic to the myapp service:
tunnel: <TUNNEL_ID>
ingress:
- hostname: myapp.example.com
service: http://myapp.myapp.svc.cluster.local:80
- service: http_status:404
7. Prisma in Containers
When using Prisma ORM, we need to handle migrations carefully:
# Run migrations as a Kubernetes Job before deploying
kubectl create job prisma-migrate --image=registry.example.com/myapp:v1.0.0 \
-- npx prisma migrate deploy
Or as a pre-deploy Job in the manifests:
apiVersion: batch/v1
kind: Job
metadata:
name: prisma-migrate
namespace: myapp
spec:
template:
spec:
containers:
- name: migrate
image: registry.example.com/myapp:v1.0.0
command: ["npx", "prisma", "migrate", "deploy"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url
restartPolicy: Never
backoffLimit: 3
8. Build and Deploy Script
#!/usr/bin/env bash
set -euo pipefail
TAG=$(git rev-parse --short HEAD)
REGISTRY="registry.example.com/myapp"
echo "Building image with tag: $TAG"
docker build \
--build-arg NEXT_PUBLIC_API_URL=https://api.example.com \
-t "$REGISTRY:$TAG" \
-t "$REGISTRY:latest" .
echo "Pushing image..."
docker push "$REGISTRY:$TAG"
docker push "$REGISTRY:latest"
echo "Updating deployment..."
kubectl set image deployment/myapp myapp="$REGISTRY:$TAG" -n myapp
echo "Waiting for rollout..."
kubectl rollout status deployment/myapp -n myapp --timeout=300s
echo "Deployment complete!"
Deploying Next.js to Kubernetes requires attention to standalone output configuration, proper Dockerfile layering, health checks, and graceful shutdown. With the manifests in this guide, we get rolling deployments with zero downtime, automatic scaling based on CPU and memory, and secure exposure via Cloudflare Tunnel. The complete setup runs reliably on even a single K3s node for smaller projects and scales horizontally across multiple nodes for high-traffic applications.
Frequently asked questions
How do I configure Next.js 16 for Docker deployment?
Set output to standalone in next.config.ts, which bundles only the files the app needs to run and cuts the image size drastically. The standalone folder leaves out .next/static and public, so the Dockerfile has to copy both in separately. The container then starts with node server.js.
What does a production Dockerfile for Next.js 16 look like?
A production Next.js Dockerfile uses three stages on node:22-alpine. The deps stage runs npm ci --ignore-scripts and npx prisma generate, the builder runs npm run build with any NEXT_PUBLIC_ build args, and the runner copies the standalone output, static files and public folder, then runs as a non-root nextjs user with UID 1001. The final image is typically 150-200 MB, against 1 GB or more for a naive Dockerfile.
Why are NEXT_PUBLIC environment variables missing in a Next.js Kubernetes deployment?
NEXT_PUBLIC_ variables are inlined into the client bundle at build time, so they must be present during npm run build. Pass them as Docker build args, for example docker build --build-arg NEXT_PUBLIC_API_URL set to the API address, with a matching ARG and ENV in the builder stage. Server-side variables are read from process.env at runtime and go in the Kubernetes Deployment, with values such as DATABASE_URL pulled from a Secret.
How do I add a health check endpoint to Next.js for Kubernetes probes?
Create app/api/health/route.ts with a GET handler that returns NextResponse.json with status ok and HTTP 200, and add a database connectivity check if needed. Point both probes at /api/health on port 3000. A workable pattern is a readiness probe every 10 seconds after a 5-second delay and a liveness probe every 30 seconds after 10 seconds, each with failureThreshold 3.
How do I set up autoscaling for Next.js on Kubernetes?
A HorizontalPodAutoscaler on autoscaling/v2 can keep between 3 and 15 replicas, with targets of 70 percent average CPU utilization and 80 percent average memory utilization. Scale-down is slowed by a 300-second stabilization window and a policy that removes at most one pod every 60 seconds. Each pod requests 200m CPU and 256Mi of memory, with limits of 1 CPU and 512Mi.
How do I expose Next.js on K3s with Cloudflare Tunnel?
Run cloudflared inside the cluster as a Deployment with two replicas of the cloudflare/cloudflared image, started with tunnel --no-autoupdate run --token and the token read from a Kubernetes Secret. The tunnel's ingress rules send the app hostname to the internal service, myapp.myapp.svc.cluster.local on port 80, with http_status:404 as the catch-all. No cloud load balancer is needed in front of the cluster.
How do I run Prisma migrations for Next.js on Kubernetes?
Migrations go in a Kubernetes Job that runs npx prisma migrate deploy with the app image before the new version rolls out. A one-off kubectl create job prisma-migrate --image=registry.example.com/myapp:v1.0.0 -- npx prisma migrate deploy works, as does a batch/v1 Job in the manifests with DATABASE_URL from a Secret, restartPolicy Never and backoffLimit 3. The image itself must copy the generated .prisma and @prisma folders into the runner stage.
How do I roll out a new Next.js image on Kubernetes without downtime?
Tag each build with git rev-parse --short HEAD, push it, run kubectl set image deployment/myapp with the new tag in the myapp namespace, and wait on kubectl rollout status with --timeout=300s. Zero downtime comes from the Deployment, which uses maxSurge 1 and maxUnavailable 0, a 30-second termination grace period and a preStop hook that sleeps 5 seconds before shutdown.
Read how we handle it in Cloud & Server Management.
Related Articles
Next.js on K8s: Solving the 5 Most Common Production Issues
Five common production issues when running Next.js on Kubernetes and how to fix each one: missing CSS with standalone output, image optimization in containers, ISR with shared cache, Node.js memory leaks, and graceful shutdown.
Next.jsNext.js Caching Layers and Why Most Teams Get Them Wrong
Next.js ships with four distinct caching layers that interact in non-obvious ways. Understanding each layer - and the common misconfigurations that defeat them - is the difference between a fast app and one that confuses developers and wastes infrastructure spend.
Next.jsMeasuring Real-World Next.js Performance Beyond Lighthouse
Lighthouse scores and lab metrics tell you what is theoretically possible. Real-user monitoring tells you what is actually happening. This article covers the tools, metrics, and instrumentation setup that give you accurate Next.js performance data in production.