Dockerfiles can be tricky to write so We need to be careful about the files.
One of the most challenging things about building images is keeping the image size down:
- We can decide to limit the files that we push to the images. This involves filtering out unwanted files.
- Using multi-stage builds while generating the image.
- Removing unwanted files after their use-case has been exhausted.
Introduction to Multi-stage builds
Multi-stage build is a very popular approach to bringing down the size of the image. To write a really efficient Dockerfile, you would need to employ shell tricks and other logic to keep the layers as small as possible and to ensure that each layer has the artifacts it needs from the previous layer and nothing else.
Multi-stage builds use multiple FROM statements that can be used with a different base, and each of them begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image. Thus ensuring that the final images are as lean as possible.
standaloneApplication in Next JS
Before we can go ahead with writing the Dockerfile, we need to understand a very powerful feature of the Next framework.
Ideally, when we build an app in Next, we continue to have a dependency on the node_moudules and in turn on the package.json. This means that even in the final image, we are required to have all the dependencies imported for the app to run successfully.
This is where the Standalone in NextJS comes into play. Next.js can automatically create a standalone folder that copies only the necessary files for a production deployment including select files in node_modules.
we can enable it in your next.config.js
module.exports = {
output: 'standalone',
}
This will create a folder at .next/standalone which can then be deployed on its own without installing node_modules.
Additionally, a minimal server.js file is also made available, which can be used instead of next start.
This minimal server does not copy the public or .next/static folders by default. These folders can be copied to the standalone/public and standalone/.next/static folders manually, after which server.js file will serve these automatically.
Dockerfile
Let’s build the Dockerfile one step at a time:
- Let us create our first layer — dependencies, where we will resolve all the dependencies and define a base. You can select your own base from the available list.
FROM node:16-alpine AS dependencies
- Let’s add a native package, set the working directory, and copy package.json and package-lock.json to define the dependencies.
RUN apk add --no-cache libc6-compat
WORKDIR /home/app
COPY package.json ./
COPY package-lock.json ./
- Once we have set ourselves up, we can go ahead and install the dependencies.
RUN npm i
We are done with adding all the dependencies. Our first layer is ready.
- Let’s create our second layer — builder. The goal of this layer is to generate the static files that we will eventually push to the final image.
FROM node:16-alpine AS builder
- We will then need to copy the relevant artifacts from the previous layer into the builder and also set the current working directory.
WORKDIR /home/app
COPY --from=dependencies /home/app/node_modules ./node_modules
COPY . .
- We would now want to build the application.
RUN npm run build
This marks the end of the second layer. At this stage, we have the static files which we will push to our final image.
- Let’s create our third and last layer — runner. Here we would push the static files and try to keep the size of the image low.
We also set the working directory and set NEXT_TELEMETRY_DISABLED to true. This will ensure that we opt out of the anonymous data collection by Next.
FROM mhart/alpine-node:slim-14 AS runner
WORKDIR /home/app
ENV NEXT_TELEMETRY_DISABLED 1
- Then we put the relevant artifact into the image. This involves the standalone files from .next/standalone, the public files, and the static files.
COPY --from=builder /home/app/.next/standalone ./standalone
COPY --from=builder /home/app/public /home/app/standalone/public
COPY --from=builder /home/app/.next/static /home/app/standalone/.next/static
- Finally, we expose the desired port and write down the final command to bring up the application.
EXPOSE 9454
ENV PORT 9454
CMD [“node”, “./standalone/server.js”]
Our Dockerfile would look something like this:
FROM node:16-alpine AS dependencies
RUN apk add --no-cache libc6-compat
WORKDIR /home/app
COPY package.json ./
COPY package-lock.json ./
RUN npm i
FROM node:16-alpine AS builder
WORKDIR /home/app
COPY --from=dependencies /home/app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
ARG NODE_ENV
ENV NODE_ENV=”${NODE_ENV}”
RUN npm run build
FROM mhart/alpine-node:slim-14 AS runner
WORKDIR /home/app
ENV NEXT_TELEMETRY_DISABLED 1
COPY --from=builder /home/app/.next/standalone ./standalone
COPY --from=builder /home/app/public /home/app/standalone/public
COPY --from=builder /home/app/.next/static /home/app/standalone/.next/static
EXPOSE 9454
ENV PORT 9454
CMD [“node”, “./standalone/server.js”]
Final Take
Below are the common mistakes:
Push the whole build file to the container
Installing all the dependencies or copying all the dependencies / node_modules to the final image.
Copying all the artifacts into the final layer including the source files.
We need to understand the relevance of each build file before we decide to push it to the image.
by following these things in the next.config.js and Dockerfile, able to narrow down the size of the Docker image from 2.19 GB to just 90.23 MB.


Top comments (0)