Dockerize a Next.js App With Standalone Output

Suman SharmaSuman Sharma

If you want to run Next.js somewhere other than Vercel, such as a VPS, Kubernetes, Fly.io or Railway, a Docker image is the most portable unit. The trick to keeping it small is output: "standalone".

Step 1: Turn on standalone output

// next.config.ts import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", }; export default nextConfig;

Now next build writes .next/standalone/ with a server.js and a trimmed node_modules containing only the files your app imports. You run it with node server.js, no next start needed.

Step 2: The Dockerfile

This version uses pnpm. For npm, swap the install commands.

# syntax=docker/dockerfile:1 FROM node:22-alpine AS base RUN corepack enable # 1. Install dependencies FROM base AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile # 2. Build FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ARG NEXT_PUBLIC_SITE_URL ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL ENV NEXT_TELEMETRY_DISABLED=1 RUN pnpm build # 3. Run FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV PORT=3000 ENV HOSTNAME=0.0.0.0 RUN addgroup -S nodejs && adduser -S nextjs -G nodejs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 CMD ["node", "server.js"]

And a .dockerignore, which matters as much as the Dockerfile:

node_modules .next .git .env* *.log

Without it, COPY . . sends your local node_modules and .env files into the build context.

Step 3: Build and run

docker build --build-arg NEXT_PUBLIC_SITE_URL=https://example.com -t my-app . docker run -p 3000:3000 --env-file .env.production my-app

The three things people get wrong

1. Missing static files

.next/standalone does not include public/ or .next/static/. If your page loads without CSS, or images 404, check those two COPY lines.

2. Build-time vs runtime environment variables

  • NEXT_PUBLIC_* values are baked into the client JavaScript at build time. Changing them in docker run -e does nothing for the browser. Pass them as --build-arg, or build one image per environment.
  • Server-only variables (DATABASE_URL, API keys) are read at runtime. Pass them with -e or --env-file, and never bake them into the image with ENV.

3. Binding to localhost

Inside a container, a server listening on localhost isn't reachable from outside. HOSTNAME=0.0.0.0 makes the standalone server listen on all interfaces.

Keeping the image small

  • Multi-stage builds mean the final image has no dev dependencies, source files, or build cache.
  • node:22-alpine is small. If a native dependency fails on Alpine (musl), switch to node:22-slim.
  • Check what's inside: docker image ls my-app and docker history my-app.

Image optimisation and caching

next/image works in standalone mode. Next.js uses sharp for optimisation in production; recent versions install it automatically, but if you see a warning about it, add sharp as a dependency.

If you run more than one container, each one has its own cache on disk. ISR and the data cache won't be shared across replicas unless you configure a shared cache handler. For a single container behind a reverse proxy, the default is fine.

Health check

HEALTHCHECK --interval=30s --timeout=3s \ CMD wget -qO- http://localhost:3000/api/health || exit 1

Add a tiny /api/health route that returns 200.

Frequently asked questions

What does output standalone do in Next.js?

It makes next build produce a .next/standalone folder with a minimal server.js and only the node_modules files the app actually uses, so the production image doesn't need the full dependency tree.

Why are my images and CSS missing in the Docker container?

The standalone folder doesn't include public/ or .next/static. You have to copy both into the image yourself, next to server.js.

Can I change NEXT_PUBLIC variables at runtime in Docker?

Not by default. NEXT_PUBLIC_ variables are inlined into the JavaScript at build time. Pass them as build args, or read config from the server at runtime instead.

Contact

Get in Touch

Want to call me? book a call and I'll respond in the available time slot. I will ignore all soliciting.