Make your Next.JS Docker images microscopic!
Published on , 1066 words, 4 minutes to read
Do standalone builds on Alpine
A series of brown and white balls of light on a black background, the result of intentionally defocusing the lens against a chandelier - Photo by Xe Iaso, Helios 44-2 58mm f/2So you've been hacking up a Next.JS app and you wanna put it into production (be it on Fly.io, Kubernetes, or whatever). Docker (or at least Docker images) are the least common denominator. If you can shove your app into a Docker image, it'll run pretty much anywhere.
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-slim as base
# Next.js app lives here
WORKDIR /app
# Set production environment
ENV NODE_ENV="production"
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build node modules
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3
# Install node modules
COPY --link package-lock.json package.json ./
RUN npm ci --include=dev
# Copy application code
COPY --link . .
# Build application
RUN npm run build
# Remove development dependencies
RUN npm prune --omit=dev
FROM base AS run
# Copy built app
COPY /app /app
# Run the app
CMD [ "npm", "run", "start" ]
To be honest, this is really good from a Dockerfile optimization standpoint. You have two stages for the build, one of them builds the app and then the run stage copies over the built app and starts it in production. There's honestly not much that can be gained by doing further optimization at the Dockerfile level.
The main problem is that the resulting image is a gigabyte:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
chonker latest 0548acbc5aa9 About a minute ago 1.01GB
These size numbers are for aarch64 (arm64) Linux Docker images. Apparently amd64 Linux Docker images should be slightly smaller. Either way, don't focus on the exact numbers too hard.
1 gigabyte to do nothing but show the default "hello world" page? That seems wasteful. Especially for an image that will be pushed and deployed multiple times per day. If you use a platform that charges you per gigabyte of container image registry space, that's a classic recipe for unbounded cost growth.
This is not good for financial solvency.
Most of the damage seems to be contained to the /app
folder:
$ docker run -it --rm --entrypoint /bin/bash chonker
root@5e8843246475:/app# cd ..
root@5e8843246475:/# du -hs app
497M app
Standalone builds
One of the quickest wins is enabling standalone builds in your next.config.mjs
file:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
};
export default nextConfig;
This makes next build
crap out the minimal subset of dependencies you need in production at /app/.next/static
. Once you enable standalone mode in your configuration, then you need to change your COPY
commands in the run
layer to something like this:
FROM base AS run
# Copy standalone app
COPY /app/.next/standalone /app
COPY /app/.next/static /app/.next/static
# Omit me if you don't have static files in your public folder yet
COPY /app/public /app/public
This cuts out even more:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
standalone latest a55c81de1fc9 8 seconds ago 363MB
That saved a whole 550 megabytes of space! However, we can do more.
Alpine Linux
Depending on the needs of your workload, you can likely get away with basing your image on Alpine Linux. In order to use that, you need to change your Dockerfile a little.
First, change the FROM
directive at the top of the Dockerfile to make everything based on node:${NODE_VERSION}-alpine
:
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine AS base
When the Dockerfile is being evaluated, this will expand out to node:22-alpine
. This lets you bump your container image's version of Node.js in lockstep with the version you use in development on your MacBooks. You can also replace the node version by using the --build-arg
flag:
$ docker build --build-arg NODE_VERSION=18 -t chonker-18 .
Then, in the build
stage, change the apt-get
command into an apk
command:
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build node modules
RUN apk -U add build-base gyp pkgconfig python3
When you build this, you get absolutely puny Docker images:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
standalone-alpine latest 3a51c7bea5e6 About a minute ago 243MB
Even better, the base image gets shared between builds, so when you push images you only really push the changes in the /app
folder!
$ docker run --rm -it --entrypoint /bin/sh standalone-alpine
/app # du -hs /app
24.2M /app
That means that when you push new versions of your app to prod, you're only really pushing about 24 MB of data. This makes your deploys faster and saves you space on the registry.
Going deeper
However, we can go deeper, we have the technology. The Node 22.x.y image uses Alpine Linux version 3.19. You can install the version of Node in Alpine's repos and make your image even smaller!
Change your FROM
directive to alpine:3.19
:
FROM alpine:3.19 AS base
Then add nodejs
and npm
to your apk add
command:
# Install packages needed to build node modules
RUN apk -U add build-base gyp pkgconfig python3 nodejs npm
Install nodejs
in the run
image:
FROM base AS run
# Install node.js
RUN apk add nodejs
And finally change the command to start Next.js directly:
# Run the app
CMD [ "node", "server.js" ]
This cuts out even more from the image, leaving you with a microscopic image:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
standalone-alpine-bean latest f37491197fbb 32 seconds ago 123MB
This is perfect to deploy to production.
Next steps
From here you can do anything you want with confidence that you aren't going to break the bank by continuously deploying to production.
If you want to see all of the examples in fully complete and working forms, check out the GitHub repo Xe/nextjs-image-optimizations.
Otherwise, have a great day! If you're going to be at Small Data SF tomorrow, I'm going to be doing a workshop about how to make something like Mystery Science Theater 3000 with vision models. It'll be a hoot.
Facts and circumstances may have changed since publication. Please contact me before jumping to conclusions if something seems wrong or unclear.
Tags: