Back to Blog
Tutorial10 min read2026-07-04

How to Deploy Next.js as a Standalone Docker App

Next.js standalone output produces a self-contained server with minimal dependencies, ideal for tiny Docker images. Here's how to build it correctly, handle env vars, and avoid the common image-bloat traps.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Most Next.js hosting advice assumes you're deploying to a platform with first-class Next.js integration. But plenty of teams want or need to run Next.js as a plain container — for portability, for a self-hosted environment, or because they're running the full Node server (SSR, API routes, middleware) rather than a static export. Next.js's standalone output mode makes this clean. This guide covers doing it right.

Static export vs standalone server

First, decide what kind of Next.js app you have:

  • Static export (output: 'export') — purely static HTML/JS, no server. Deploy it to any static host or CDN. No Docker needed.
  • Standalone server (output: 'standalone') — a Node.js server that handles SSR, API routes, ISR, and middleware. This is what you containerize.

If your app has no dynamic server logic, prefer the static route — it's cheaper and faster. On PandaStack, a Next.js static export deploys as a static site (built in pandastack.ai microVMs) with automatic SSL and CDN. The rest of this guide assumes you need the server.

Enabling standalone output

In next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
};
module.exports = nextConfig;

When you run next build, Next.js traces exactly which node_modules files are actually used and copies them, plus a minimal server.js, into .next/standalone. This is what makes the image tiny: you don't ship your entire node_modules.

The Dockerfile

A correct multi-stage Dockerfile for standalone output:

# --- deps ---
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# --- build ---
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# --- runtime ---
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

RUN addgroup -g 1001 nodejs && adduser -u 1001 -G nodejs -S nextjs

# Copy only the standalone output
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public

USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME=0.0.0.0
CMD ["node", "server.js"]

The three COPY lines from the build stage are the part people get wrong:

  1. 1.next/standalone — the traced server and minimal deps
  2. 2.next/static — your built JS/CSS chunks (NOT included in standalone automatically)
  3. 3public — static assets

Forget the .next/static copy and your app loads but serves no CSS or client JS. This is the #1 standalone gotcha.

The env var trap

Next.js has two kinds of environment variables, and conflating them causes hours of confusion:

  • NEXT_PUBLIC_* — inlined into the client bundle at build time. Changing them requires a rebuild. They are baked into the JS shipped to browsers.
  • Server-only vars (no prefix) — read at runtime in server code (API routes, server components).

The practical consequence: if you set NEXT_PUBLIC_API_URL only as a runtime env var on your host, it won't appear in the client bundle. It must be present during next build. On a platform that builds from your repo, set build-time public vars in the build environment, and runtime secrets as runtime env vars. On PandaStack, env vars are available to both the build job and the running container, so the standard pattern works without surprises.

Deploying

Connect your repo to a host that builds from a Dockerfile. PandaStack runs the build with rootless BuildKit in an ephemeral Kubernetes Job, pushes the image to the registry, and deploys via Helm. Configure:

  • Port: 3000 (matching the Dockerfile)
  • Health check: add a tiny route like app/api/health/route.ts returning 200
  • Env vars: split build-time (NEXT_PUBLIC_*) and runtime as above
// app/api/health/route.ts
export function GET() {
  return new Response('ok', { status: 200 });
}

Image and runtime considerations

  • Image size: with standalone, expect ~150–250 MB on the Node Alpine base, versus 1 GB+ if you ship full node_modules. Alpine keeps the base small; if you hit native-module issues, fall back to node:20-slim.
  • ISR and the filesystem: Incremental Static Regeneration writes to the filesystem by default. In a multi-replica container deployment, each replica has its own ephemeral filesystem, so regenerated pages aren't shared. For multi-instance ISR, configure a shared cache handler or accept per-instance regeneration. For single-instance or low-traffic apps this rarely matters.
  • Sharp for images: if you use next/image optimization, ensure sharp is installed; on Alpine it usually works out of the box with recent Node images.
  • Cold starts: Node apps have a warmup cost. On a scale-to-zero free tier, the first request after idle pays it. For consistently low latency, keep an instance warm.

Standalone vs platform-native

The standalone container approach is maximally portable — the same image runs anywhere. The tradeoff is you manage the Dockerfile and miss some platform-native niceties (automatic ISR caching, edge middleware specialization). For most self-hosted SSR Next.js apps, standalone in a container is the right balance of control and simplicity.

References

  • Next.js Docker deployment: https://nextjs.org/docs/app/building-your-application/deploying#docker-image
  • Next.js output config: https://nextjs.org/docs/app/api-reference/next-config-js/output
  • Next.js environment variables: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
  • Official Next.js Docker example: https://github.com/vercel/next.js/tree/canary/examples/with-docker
  • node Alpine images: https://hub.docker.com/_/node

---

Shipping a standalone Next.js container? PandaStack builds your Dockerfile with rootless BuildKit, handles SSL and custom domains, and offers a free tier for both static exports and server containers. Get started at https://dashboard.pandastack.io.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also