Maurício Mutte
BlogAbout

Creating an Optimized Next.js Docker Image

Using Docker with Next.js applications is an excellent way to facilitate team development and simplify deployment — a strategy even suggested by the framework’s official documentation.

To get started, let’s create a basic Docker image for a Next.js application. Since Next.js runs on Node.js, we’ll use a Node.js Docker image as our base. Additionally, we’ll perform these steps to make the application work on Docker:

  1. Copy the package.json file into the Docker environment
  2. Install dependencies
  3. Copy the project files
  4. Build the application
  5. Run the application

Here’s what the resulting Dockerfile might look like:

Dockerfile
FROM node:22

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm ci

COPY . .

RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]

While functional, this Dockerfile can be significantly optimized to reduce the image size. A large Docker image can lead to slower builds, higher storage costs, and longer deployment times. Below, I’ll walk through some strategies to slim it down.

print running docker image ls

Strategies to reduce image size

Strategy 1: use a smaller base image

The node:22 image is versatile but also heavy because it’s built on a full Linux distribution. By switching to a smaller variant like node:22-alpine, we can significantly reduce the base size of the image

Dockerfile
FROM node:22-alpine

print running docker image ls

With this small change, the size of the image has decreased dramatically. From 2.23 GB to 1.27 GB - a reduction of almost 50%!

Strategy 2: Multi-stage builds

Multi-stage builds let us separate the build process from the final runtime environment, ensuring that only the essential files end up in the final image:

Dockerfile
FROM node:22-alpine AS base

# Stage 1: Install dependencies
FROM base AS deps
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci

# Stage 2: Build the source code
FROM base AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build

# Stage 3: Run the app
FROM base AS runner
WORKDIR /app

COPY --from=builder /app/next.config.ts ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next

EXPOSE 3000
CMD ["npm", "start"]

By doing this, we’re excluding unnecessary files from the final image, such as development dependencies and source files that aren’t required for runtime.

print running docker image ls

Now our image has 874MB. Much better! But we can still improve a bit more...

Strategy 3: Next.js standalone output

Next.js offers a standalone output mode, which separates only the necessary files at build time and creates a Node.js server for deployment. Here’s how to configure it:

next.config.js
const nextConfig = {
  output: "standalone"
};
 
export default nextConfig;

Final Dockerfile

The standalone mode creates a /standalone directory in the .next output, so we need to copy it into the Docker image:

Dockerfile
FROM node:22-alpine as base

# Install dependencies
FROM base as deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Build the source code
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Run the application in production mode
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

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

ENV PORT=3000

# server.js is created by next build from the standalone output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

This optimized Dockerfile results in a final image size of 232MB. Compared to our original 2.23GB image, that’s a staggering reduction of nearly 90%!!! 🚀

print running docker image ls with all images

It’s worth noting that this version closely mirrors the example provided in the official Next.js repository

Conclusion

Using Docker to run Next.js applications is vital for modern development workflows, and optimizing Docker image sizes can dramatically improve build times, reduce storage costs, and streamline deployments. The strategies outlined here can help you achieve leaner and more efficient Docker images for your Next.js projects.

I hope you found this guide helpful! 🚀