Next.js ships a feature called standalone output that's purpose-built for Docker: turn it on in next.config.js and the build produces a self-contained server.js plus only the modules it actually needs. The resulting Docker image is under 200 MB instead of the 1+ GB you'd get by shipping the whole project.
A working Dockerfile
# 1. Dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# 2. Build
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# 3. Runtime
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1
# Create the non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy the standalone output + static + public
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=build --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME=0.0.0.0
CMD ["node", "server.js"]You also need to opt into standalone output in next.config.js:
module.exports = {
output: 'standalone',
};That's the whole recipe. Build and run:
docker build -t my-next-app .
docker run -d --name my-next-app -p 3000:3000 my-next-appThe image lands around 150-180 MB depending on dependencies. Without standalone output, the same build typically lands at 1-1.5 GB.
What standalone output does
next build normally produces .next/ and expects you to ship the whole project including node_modules. With output: 'standalone', the build instead writes a .next/standalone/ directory that contains:
- A
server.jsentrypoint that boots Next withoutnext start. - A trimmed
node_modules/with only the modules your app actually requires (tree-shaken from the dependency graph). - A minimal
package.json.
Everything else — devDependencies, build tools, your original .next/static, your public/ — can be left out of the runtime image. The result is the under-200-MB image.
The three things you have to copy
The standalone directory is not complete on its own. Two things have to be copied separately or your app 404s on assets:
.next/static— the per-build static assets (JS chunks, CSS, optimized images). Copy to./.next/static.public/— your project's public directory (favicon, robots.txt, etc.). Copy to./public.
The Next.js docs cover this; many tutorials skip it. Without them, your site loads HTML but every CSS file and JS chunk returns 404. The Dockerfile above gets the three copies right.
Pick the right Node image
node:22-alpine everywhere — the build doesn't need glibc (Next.js is pure-JS apart from a handful of optional native modules that work on Alpine), and the runtime image gets the size advantage. If you use Sharp for image optimization, Sharp ships prebuilt musl binaries since v0.30, so Alpine is fine.
If your project has a dependency that breaks on Alpine, switch to node:22-slim in the runtime stage:
FROM node:22-slim AS runtimeRun as non-root
The Dockerfile creates a nextjs user with UID 1001. Why a custom user instead of the image's default node (UID 1000)?
Convention. The Next.js docs use 1001; matching the docs makes the recipe predictable. Either works.
USER nextjs is the line that switches; everything after it runs as the non-root user. The --chown=nextjs:nodejs on the COPY lines ensures the files are owned correctly so the runtime user can read them.
PORT and HOSTNAME
ENV PORT=3000 HOSTNAME=0.0.0.0The server.js generated by standalone output reads these. HOSTNAME=0.0.0.0 binds on all interfaces (without it, server.js defaults to localhost inside the container and the host can't reach it). PORT=3000 is the standard.
Practical usage: Compose with a database
services:
web:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:hello@db:5432/myapp
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: hello
POSTGRES_DB: myapp
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pg-data:docker compose up --build -d. The Next.js container connects to the db service via its service name.
Static export instead
If your Next.js app is fully static (no API routes, no server components that need a runtime), next export (or output: 'export' in next.config.js) writes plain HTML/CSS/JS to out/, and you can serve it with a static server like Nginx:
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/out /usr/share/nginx/html
EXPOSE 80That ships about 25 MB. Only works for fully static sites — no API routes, no SSR, no server components with runtime needs.
Development with hot reload
For dev, you want the source bind-mounted and next dev running:
# docker-compose.dev.yml
services:
web:
image: node:22-alpine
working_dir: /app
volumes:
- .:/app
- app-node_modules:/app/node_modules
ports:
- "3000:3000"
command: sh -c "npm ci && npm run dev"
environment:
NEXT_TELEMETRY_DISABLED: "1"
volumes:
app-node_modules:The named volume on node_modules keeps install fast on Mac/Windows; the bind mount on the source enables hot reload. Different file from production — keep them separate.
Common pitfalls
output: 'standalone'not enabled innext.config.js. Without it, there's no.next/standalonedirectory and the build copies fail. Set it.- Forgetting to copy
.next/staticorpublic/. Site loads HTML, every asset 404s. Both copies are mandatory. HOSTNAMEdefaulting to localhost. The container's localhost isn't your host's localhost. SetHOSTNAME=0.0.0.0explicitly.- Building Sharp from source on Alpine. Sharp ships prebuilt musl binaries; verify your version is current (v0.30+) and you won't need build tools in the image.
- Bind-mounting source in production. That's a dev pattern. Production Docker builds copy the source in and ship.
What to do next
- How to Write a Dockerfile — fundamentals.
- Docker Image Size Optimization — beyond standalone output.
- How to Run PostgreSQL in Docker — the database side for a typical Next.js full-stack app.
- How to Run Nginx in Docker — if you're shipping a fully-static export instead.
FAQ
Sources
Authoritative references this article was fact-checked against.
- Next.js — Docker deployment docsnextjs.org
- Next.js output configurationnextjs.org

