How to Cut Your Next.js Docker Image Size Like a Chef!

August 1, 2024

Is your Dockerfile a hot mess of unnecessary bulk? Let's get those Next.js images lean and mean without losing any flavor!

When you're self-hosting a Next.js app outside of Vercel, you've got a few options to choose from:

  • Running it on a Node.js server
  • Wrapping it in a Docker container
  • Doing a static export

I'm all about Docker containers because they're so versatile. But when you're running a bunch of containers on a VPS, the size of each Docker image starts to matter a lot. You want to keep them as small as possible to save space. In this article, we'll explore how to slim down the Docker image size for your Next.js app without sacrificing functionality.

Noob Dockerfile

A couple of years back when I was new to self-hosting Next.js apps, I'd whip up a quick Dockerfile and call it a day (yep, that was me, the noob). I never gave the image size a second thought. But when I started hosting multiple apps on a tiny VPS, my free space began to vanish. Some apps, packed with dependencies, were gobbling up 3 GB each! I knew there had to be a way to reduce the size of these Docker images, so I started exploring. Let's take a look at a simple Noob Dockerfile to see where I was going wrong.

First create a new Next.js app:

npx create-next-app@latest docker-next
cd docker-next

Let's start by creating a simple Noob Dockerfile and see just how bloated it can get.

FROM node:20.9-alpine3.17
WORKDIR /app

COPY package*.json ./
RUN npm install -g npm@10.1.0

COPY . .
RUN npm install && npm run build

EXPOSE 3000
CMD npm run start

Put that Docker image in the oven:

docker build -f Dockerfile.noob -t next:noob .

Voilà, the next:noob Docker image is served at a chunky 854 MB. That's way too big for a simple app, more like a full-course meal when we just wanted a light snack. Time to sharpen our knives and cut this down to size!

Expert Dockerfile

Before we dive into crafting our Expert Dockerfile, there’s one small but crucial prep step we need to tackle: updating the next.config.mjs file. This update will ensure that our Next.js app is optimized for a slimmer Docker image. All you need to do is add the output: "standalone" configuration, which will bundle your app into a single, more efficient output. Here’s how you do it:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "standalone",
};

export default nextConfig;

With this setup, our app is primed and ready for a finely-tuned Docker experience. Now, let's whip this Docker image into shape by slicing it into multiple stages, just like prepping a meal. Here's the recipe:

  1. base - Only install the essentials when necessary.
  2. deps - Use the preferred package manager to fetch ingredients, er, dependencies.
  3. builder - Cook up the source code only when it's fresh.
  4. runner - Serve the final dish by copying the necessary files and running Next.js.
# Stage 1: Build the Next.js application
FROM node:20.9-alpine3.17 as base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN   if [ -f yarn.lock ]; then yarn --frozen-lockfile;   elif [ -f package-lock.json ]; then npm ci;   elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile;   else echo "Lockfile not found." && exit 1;   fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1

RUN   if [ -f yarn.lock ]; then yarn run build;   elif [ -f package-lock.json ]; then npm run build;   elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build;   else echo "Lockfile not found." && exit 1;   fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1

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

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
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
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]

Next up, we’ll add a .dockerignore file to prevent extra files from sneaking into the Docker image.

# Node.js
node_modules
npm-debug.log
yarn-error.log
yarn.lock

# Next.js
.next
out
.next/cache
*.log

# Local development
.git
.gitignore
.dockerignore
Dockerfile
.vscode
README.md
LICENSE
*.md

# OS files
.DS_Store
Thumbs.db

# Environment files
.env.local
.env.development
.env.test
.env
.env.production

# Ignore npm log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

Let's cook up our gourmet Docker image:

docker build -t next:expert .

The next:expert Docker image weighs in at just 154 MB. That's over 80% smaller than the noob version! It's like going from a heavy stew to a light, flavorful broth. And for larger apps with tons of dependencies, the savings can be even more impressive.

Conclusion

Final Docker image sizes

And that's how you trim the fat from your Docker images! Breaking down your Dockerfile into stages is like following a good recipe: it makes everything more efficient and delicious. By doing this, we've managed to cut our Docker image size down to a lean 154 MB, leaving more room on your VPS for other tasty apps. I hope this guide helps you serve up some lightweight, speedy Next.js apps. Got any questions or just want to chat? Hit me up on Twitter.

Author: David Slaninka

“Get ready” for the upgrade_

Let's craft a website that leaves a lasting legacy. Your journey starts now.

©2024 BACHOFF Studio

All Rights Reserved