Next.js App Router in Production · Streaming, Security and Operations
Self-Hosting: Standalone Output, Health, after() and Headers
Run Next.js in production on your own servers: standalone output, security headers, health endpoints, after() for post-response work, and shared caches.
What you'll build
A production configuration: standalone output for a small container image, security headers for every route, no X-Powered-By header, a health endpoint, and an orders API that sends its confirmation email with after(), after the response has gone out.
The idea
output: "standalone"produces.next/standalonewith a minimalserver.jsand only the needednode_modules; copy it withpublic/and.next/staticinto the image and runnode server.js.headers()innext.config.tsadds headers such as HSTS,X-Content-Type-Optionsand a Content Security Policy.after()runs work after the response is sent (logging, emails, analytics) without delaying the user; the server waits for pendingaftercallbacks during graceful shutdown.- With several instances, use a shared cache handler (
cacheHandlerswith"use cache: remote") and setdeploymentIdso rolling deployments don't mix assets from different builds.
The code
// file: next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
output: "standalone",
poweredByHeader: false,
deploymentId: process.env.DEPLOYMENT_ID,
async headers() {
return [{
source: "/:path*",
headers: [
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
],
}];
},
};
export default nextConfig;
// file: .env
DEPLOYMENT_ID=2026-10-26-3
// file: app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html lang="en"><body><main>{children}</main></body></html>;
}
// file: app/api/health/route.ts
import { connection } from "next/server";
export async function GET() {
await connection(); // run per request, never prerendered
const databaseUp = true; // a real check pings the database with a short timeout
return Response.json(
{ status: databaseUp ? "ok" : "unavailable", deployment: process.env.DEPLOYMENT_ID },
{ status: databaseUp ? 200 : 503, headers: { "Cache-Control": "no-store" } },
);
}
// file: app/api/orders/route.ts
import { after } from "next/server";
export async function POST(request: Request) {
const { item } = await request.json();
const order = { id: "o42", item };
after(async () => {
await new Promise((resolve) => setTimeout(resolve, 100)); // e.g. send the confirmation email
console.log(`[app] confirmation email sent for ${order.id}`);
});
console.log(`[app] order ${order.id} saved, responding`);
return Response.json(order, { status: 201 });
}
// headers: x-powered-by, strict-transport-security, x-content-type-options, cache-control
// request: GET /api/health
// request: POST /api/orders {"item": "Teapot"}
// wait: 0.3Output
GET /api/health -> 200 (strict-transport-security: max-age=63072000; includeSubDomains; preload, x-content-type-options: nosniff, cache-control: no-store)
{"status": "ok", "deployment": "2026-10-26-3"}
POST /api/orders -> 201 (strict-transport-security: max-age=63072000; includeSubDomains; preload, x-content-type-options: nosniff)
[app] order o42 saved, responding
{"id": "o42", "item": "Teapot"}
[app] confirmation email sent for o42Run this on your computer — see “Get Started” for setup.
Notice there's no x-powered-by header. The order response is sent before the email work runs; its log line appears after the "responding" line. In a container you'd run node .next/standalone/server.js rather than next start.
Common mistakes
- Shipping the full repository and
node_modulesin the image instead of the standalone output. - Several instances with the default in-memory cache: users see different data and tag invalidation reaches one instance only.
- Doing slow side effects before returning the response instead of in
after()or a queue. - Killing the process immediately on deploy; send SIGTERM and allow a drain period (10-30 s) so in-flight requests and
after()work finish. - Putting secrets in
NEXT_PUBLIC_variables or baking them into the image at build time.
Key points
- Standalone output for small images; security headers in next.config.
- Health endpoints run per request and don't get cached.
- after() for post-response work; shared caches and deploymentId for multiple instances.
Further reading: Self-hosting · output: standalone · after · Production checklist
Practice questions
What does output: "standalone" produce?
Answer
A .next/standalone folder with a minimal server.js and only the node_modules files the app needs, for small deployment images (public/ and .next/static are copied alongside).
What is after() for?
Answer
Scheduling work to run after the response is sent, such as logging or sending emails, so it doesn't delay the user.
What changes when you run several Next.js instances?
Answer
Caches must be shared (custom cache handlers, use cache: remote) so all instances see the same data and invalidations, and deploymentId helps handle version skew during rolling deployments.
Open in the app to mark lessons complete, take notes and track your progress.