From b8e352ba22e4d603f7136fe7a6bb31a43e024aa8 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 22:39:14 +0100 Subject: [PATCH] Use a multi-stage Docker build with Next's standalone output Install with npm ci, ship only the standalone server and its traced dependencies, and run as a non-root user. Shrinks the image from 1.78 GB to 438 MB. Co-Authored-By: Claude Opus 5.5 --- Dockerfile | 34 +++++++++++++++++++++++++++++----- next.config.ts | 3 +++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 885ea82..6d254dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,16 @@ -FROM node:26-alpine +FROM node:26-alpine AS deps WORKDIR /app -COPY package.json ./ -COPY package-lock.json ./ -RUN npm install +COPY package.json package-lock.json ./ +RUN npm ci + +FROM node:26-alpine AS builder + +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules COPY . . # NEXT_PUBLIC_* values are inlined at build time, and pages are prerendered @@ -17,6 +22,25 @@ ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID RUN npm run build + +# Only the standalone server and its traced dependencies end up in the final +# image, not the full node_modules or the source +FROM node:26-alpine AS runner + +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=9000 +ENV HOSTNAME=0.0.0.0 + +# The server writes regenerated pages to .next, so the files must be owned by +# the user it runs as +COPY --from=builder --chown=node:node /app/.next/standalone ./ +COPY --from=builder --chown=node:node /app/.next/static ./.next/static +COPY --from=builder --chown=node:node /app/public ./public + +USER node + EXPOSE 9000 -CMD ["npm", "run", "start"] +CMD ["node", "server.js"] diff --git a/next.config.ts b/next.config.ts index 66e7bc6..73f3000 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,9 @@ import createNextIntlPlugin from "next-intl/plugin" const withNextIntl = createNextIntlPlugin() const nextConfig: NextConfig = { + // Bundles a minimal server with only the needed dependencies, used by the + // Docker image + output: "standalone", images: { remotePatterns: [ {