DEV Community

Cover image for How to: Use Next.js Standalone for Leaner Docker Images
Michelle Mendoza
Michelle Mendoza

Posted on

How to: Use Next.js Standalone for Leaner Docker Images

Building a Docker image from a fresh Next.js app can easily bloat your output to over 890+ MB and that size comes at a cost. Bigger images mean slower CI/CD pipelines, thanks to the extra time needed to pull and push them.

But, this can be resolved with the introduction of the standalone feature of Next.js. What standalone does is it creates a .next/standalone folder that packages only the production-critical files and excludes dev dependencies and unnecessary files. In this walkthrough, I will show you how I was able to achieve a 75% reduction in my image’s size by leveraging the standalone feature of Next.js in a multi-stage docker build.


🚨 Prerequisites

  • An existing Next.js app (Minimum required version is Next.js v12.2.0) or if you don’t have one, you can quickly create with npx create-next-app@latest .
  • A Docker installed in your machine. (If you currently don’t have one, you can follow this official guide form Docker on how to install)

🛠️ Walkthrough

Step 1: Enable Standalone Output

First, open your next.config.ts file in the root directory of your project and add output: "standalone". This enables your Next.js app to build with only the essential files.

Your next.config.ts file should look like this:

 const nextConfig = {
   // Your other Next.js configurations can go here
   output: "standalone",
 };

 export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Multi-Stage Dockerfile

I’ve prepared a Dockerfile for you to use. In this walkthrough, we’ll be using a multi-stage Docker build. Multi stage builds are a good practice because it creates a smaller and much secure docker image by separating the builder stage and runner stage. Here’s a quick explanation of what a multi-stage build does:

  • The builder stage: This is where we install all dependencies including dev dependencies and run the build process to generate next/standalone output. Essentially, this stage produces everything that is required for the production build.
  • The runner stage: This is the part where we create the final, production ready image. It contains only the minimal files needed to serve the app, resulting in a much smaller image. No dev dependencies, no full node modules, strictly only what is needed.
FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

# 2. Runner Stage
# Runner Stage
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=10000

EXPOSE 10000

COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

# no entrypoint needed
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

Here are the key takeaways of this Dockerfile:

  • Cache trick: Running npm install before copying the source code allows you to take advantage of Docker’s layer caching system. If anything changes in your code but not on the dependencies then this part will not run again, saving us some time.
  • Environment variables: We set environment variables to define the app’s runtime behavior. By adding NODE_ENV=production on your file, you are telling Next.js to run in the optimize production mode. Setting hostname to 0.0.0.0 is essential for a container as it allows it to listen to any outside connection.
  • The three essential COPY: These set of COPY commands is necessary in order to optimize our build. It is a common pitfall that the next/standalone folder contains everything. But by default, .next/standalone does not include your static and public folder. That is why we have to copy it in order to include it in the final build.

Step 3: Build and Run Your Image

  • Build your image using this command: docker build -t my-app .
  • Run your image using this command: docker run -p 3000:10000 my-app

You can now access your app at http://localhost:3000/

(Note: The -p 3000:10000 flag maps your local machine's port 3000 to the containers internal port 10000. This allows you to modify how you access the app without changing the internal configuration.)

📝 Conclusion

And there you have it! By combining a multi-stage build with the standalone feature, we've taken a standard 892mb Next.js image and reduced it to 229mb, a 75% reduction 🤯. While this number may vary depending on the size of your actual project, this proves that you can significantly optimize your image by using this setup, leading to a faster build and deployment times in your CI/CD pipelines.

Comparison:

Without standalone (multistage build)

Without standalone image build size

With standalone (multistage build)

With standalone image build size

Do you have any tips or tricks to further optimize your docker image build 🤔? Comment down below and share it with the others!

Top comments (0)