Dock ‘n’ Roll: Dockerize Your Node App With Multi-Stage Build

Rupen Chitroda - Aug 22 - - Dev Community

Ever wondered how to optimize your Docker images for production? With multi-stage builds, you can significantly reduce the size and complexity of your Docker images, leading to faster deployments and better resource management.

But why the heck multi-stage build?

Let's understand that referencing satellite launcher terminology 🚀. As we often see in satellite launches, multiple separations occur in the SLV (Satellite Launch Vehicle) during its journey to space. In the vehicle, booster rockets provide the energy required to break free from Earth's gravity. When the booster rockets exhaust their fuel, they are jettisoned from the vehicle.

As the SLV propels itself into space, it detaches other parts of the vehicle in stages in order to reach its destination. As the vehicle progresses into space, its weight decreases and its speed increases. Consequently, the satellite becomes lighter and can easily float in space.

Image description

Just like that, the multi-stage image works. In the initial phase, we need to install Node, TypeScript, and their dependencies, which are crucial for building the app. However, once the code is compiled, these dependencies are no longer needed in the final image. In the final stage, we keep only those files which are required to run the app. By following this approach, we are able to reduce the size of the image significantly.

Additionally, by following this approach, we are able to utilize Docker's cache, which drastically reduces the build creation time and optimizes the Docker image.

Let's create Multi-Stage Dockerfile

Build Stage (Rocket booster stage) 🚀

FROM node:20.15-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx tsc -p tsconfig.json
Enter fullscreen mode Exit fullscreen mode

With multi-stage builds, your Dockerfile includes multiple FROM statements. Each FROM command starts a new build stage and can utilize a different base image. Here node:20.15-alpine is used as base image. And we have kept build name as the name of this stage.

Alpine image is a lightweight Linux distribution that is used as a base image to create smaller and optimized docker containers.

  • WORKDIR /app: Define work directory for the docker container, all the operation executed after this step will be performed under this directory
  • COPY package*.json: Copy all package JSONs into the work directory
  • RUN npm ci: Install all the dependencies mentioned in the package*.json
  • COPY . .: Copy other files residing in the root directory of the application (You can omit this step if you don't have any depended files that will be used by the app while running)
  • RUN npx tsc -p tsconfig.json: Compile typescript files using compiler options defined in the tsconfig.json. You can directly use tsc without using npx but for that you need to install tsc at global level in the docker image which will add one more step in the Dockerfile and increase the Docker Layers and ultimately increase the size of the image.

Production Stage 🛰️

FROM node:20.15-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm i --omit=dev
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode
  • FROM node:20.15-alpine AS production: Here same base image is used which was used in the build stage and we have kept production name as the name of this stage
  • WORKDIR /app: Work directory for the container
  • COPY package*.json ./: Copy all the package JSONs
  • RUN npm i --omit=dev: Install only those dependencies which are mentioned in the dependencies property of the package.json.
  • COPY — from=build /app/dist ./dist : Copy dist folder from the build stage, it will have all the compiled files
  • COPY — from=build /app/package*.json ./ : Copy package JSONs from the build stage to run npm commands
  • CMD [“npm”, “start”] : Finally run the npm start to up and running the application

Note that npm i installs dependencies including those dependencies included in the devDependencies. We don't need those in the final outcome. So by providing the --omit flag we tell the engine to omit the dev dependencies.

So after all the detachments😉 we finally have our satellite floating in the space. And our final Dockerfile looks like this..

FROM node:20.15-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx tsc -p tsconfig.json

FROM node:20.15-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm i --omit=dev
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

A Docker image built with this Dockerfile will be constructed more quickly compared to a non-multi-stage Dockerfile, and the size of the image will also be smaller.

Image description

Here you can see the difference of the sizes between the nodeapp and nodeapp-multi-stage

Bonus: You can use distroless images too to further reduce the image size.

.
Terabox Video Player