WittCode💻

Your Multi-Stage Builds Could Be Broken

By

Multi-stage Docker builds reduce image size and improve security by separating build and runtime environments. However, they need to be implemented correctly!

Table of Contents 📖

Multi-Stage Builds

Multi-stage Docker builds reduce image size and improve security by separating the build and runtime environments. They are also a great way to demonstrate the difference between the Docker RUN and CMD instructions. Take the example below:

INFO: At its core, multi-stage builds are multiple FROM statements in a Dockerfile. The first stage includes tools for building the app and the later stage copies only the necessary artifacts from the first stage into the final image.

FROM node:22-alpine AS build
WORKDIR /node
COPY package*.json .
RUN npm i
COPY . .
CMD ["npm", "run", "build"]

FROM node:22-alpine
WORKDIR /node
COPY --from=build /node/dist/server.cjs server.cjs
COPY --from=build /node/dist/server.cjs.map server.cjs.map
CMD ["node", "--enable-source-maps", "server.cjs"]

This Dockerfile compiles a TypeScript app into JavaScript in the first stage and then copies the JavaScript artifacts from the first stage into the final image. The final image then runs the app using the Node.js. However, there is an issue here. Notice how the first build is using the CMD command? Well, the CMD command specifies the default command that runs when the container starts. This is fine in a single stage build, or the final step of a multi-stage build, but here we want the npm run build command to be ran during the build process.

ERROR: If we run the first stage with CMD, then there will be no outputted artifacts for the second stage to use.

What we need to do is use RUN instead. RUN executes commands while building the image. This means our npm run build script will run before starting the second stage. So, here is what the Dockerfile should look like.

FROM node:22-alpine AS build
WORKDIR /node
COPY package*.json .
RUN npm i
COPY . .
RUN ["npm", "run", "build"]

FROM node:22-alpine
WORKDIR /node
COPY --from=build /node/dist/server.cjs server.cjs
COPY --from=build /node/dist/server.cjs.map server.cjs.map
CMD ["node", "--enable-source-maps", "server.cjs"]