Next.js is a popular React Framework that comes with a lot of great features such as hybrid static & server rendering, smart bundling, file system routing, and much more. I prefer it over vanilla React.
When we dockerize a Next.js application, we want the final production and development images to be small, fast to build and easy to maintain. Let’s see how we can use multi-stage builds and other image optimization techniques to create the ideal images.
Stage 1: Install dependencies
FROM node:17-alpine AS deps WORKDIR /app COPY package*.json . ARG NODE_ENV ENV NODE_ENV $NODE_ENV RUN npm install
Start by installing our dependencies into a layer called
deps using the
NODE_ENV=production at image build time to prevent any dev dependencies from being installed. Per the npm install docs:
--productionflag (or when the
NODE_ENVenvironment variable is set to
production), npm will not install modules listed in
Stage 2: Build
FROM node:17-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY src ./src COPY public ./public COPY package.json next.config.js jsconfig.json ./ RUN npm run build
Create a new layer called
builder. Copy the
node_modules/ directory, which was created in the
deps stage, plus any other files you need to build your app.
We could copy everything using
COPY . . but it’s better if we only bring in what we need. This is why we’re using the Next.js src directory which consists entirely of files we need to build our app and cuts down on the number of
COPY commands. As we’ll see in the Image Sizes table below, this will dramatically reduce the final image size.
jsconfig.json file allows us to use absolute imports in our code which are much cleaner than relative imports.
Stage 3: Run
FROM node:17-alpine WORKDIR /app COPY --from=builder /app/.next ./.next COPY --from=builder /app/public ./public COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./ CMD ["npm", "run", "start"]
At this point, we have everything we need. We just have to copy in the relevant files to our final layer and run the app. Assigning the final layer a name is unnecessary. Note,
.next/ contains our application build.
public/ is not included in the build so be sure to copy it over.
# Stage 1: install dependencies FROM node:17-alpine AS deps WORKDIR /app COPY package*.json . ARG NODE_ENV ENV NODE_ENV $NODE_ENV RUN npm install # Stage 2: build FROM node:17-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY src ./src COPY public ./public COPY package.json next.config.js jsconfig.json ./ RUN npm run build # Stage 3: run FROM node:17-alpine WORKDIR /app COPY --from=builder /app/.next ./.next COPY --from=builder /app/public ./public COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./ CMD ["npm", "run", "start"]
Build the Docker images
Multi-stage builds allow us to create different images using the same
Dockerfile. We can build both our production and development images using the following
services: app-prod: build: context: . args: - NODE_ENV=production ports: - '3000:3000' app-dev: build: context: . target: deps command: npm run dev ports: - '3001:3000' environment: - NODE_ENV=development volumes: - .:/app - /app/node_modules - /app/.next
app-prod, we’re injecting
NODE_ENV=production as a build argument because Next.js environment variables have to be set at build time. Setting them at runtime only works in development.
app-dev, we’re using the
--target deps argument to tell docker to stop at the end of the
deps build stage. The Next.js dev server doesn’t use the Next.js build so there’s no need to waste time on stages 2 and 3. This reduces the build time from 90 sec to 50 sec. Once the dependencies are cached on your machine, this drops to less than a second.
One more point is we’re passing all our source code into the
app-dev container as a mounted volume so our local changes are immediately reflected in the running container. The latter two volumes tell docker not to copy the
.next/ back to the host because we don’t need them there.
Here are some image sizes to illustrate how important some of these factors are on the final result.
May your Next.js images be small, fast and maintainable.