If you have a Dockerfile which looks something like (note: likely not complete, this is a very stripped down version of our Dockerfile):

FROM node:16.19.0 as prodbuild

COPY ./package*.json ./

RUN npm ci --omit=dev

FROM node:16.19.0 as build

COPY ./package*.json ./

# We need dev dependencies to actually do the build
RUN npm ci
RUN npm run web:build

FROM node:16.19.0

COPY --from=prodbuild /usr/src/node_modules ./node_modules
COPY --from=build /usr/src/.next .next

ENTRYPOINT ["npm", "run", "start"]

There are a few inefficiencies. For one, you’re installing prod dependencies twice, once in prodbuild, and a second time in build. You’re also missing out (for local development at least) on the system-wide npm download cache. These can be fixed by updating your Dockerfile to look more like this:

FROM node:16.19.0 as prodbuild

COPY ./package*.json ./

# This (and the one below) needs to be sharing=locked as not sure what would happen if two npm commands used it simultaneously.
RUN --mount=type=cache,target=/root/.npm,sharing=locked npm ci --omit=dev

RUN npm ci --omit=dev

FROM node:16.19.0 as build

COPY ./package*.json ./

# We've already installed the node_modules for dev above, just copy them in here.
COPY --from=prodbuild /usr/src/node_modules/ ./node_modules/
RUN --mount=type=cache,target=/root/.npm,sharing=locked npm ci

RUN npm ci
RUN npm run web:build

FROM node:16.19.0

COPY --from=prodbuild /usr/src/node_modules ./node_modules
COPY --from=build /usr/src/.next .next

ENTRYPOINT ["npm", "run", "start"]

For our team, for most team members the above actually works out to be about even for the majority of the team. However, some team members (mostly those with slower internet connections) do see an improvement from the better re-use of already downloaded modules.

There are other varuations you might want to make to this depending on your specific bottlenecks. Where network speed isn’t an issue, it might actually be faster to remove the additional node_modules copy, and upate the id of the mount caches to be different for each one. This will allow the two npm commands to run in parallel at the expense of:

  1. Additional network bandwidth to download both dev & prod dependencies in the second npm ci
  2. A slower first build (and future builds where node dependencies are changed) as the two commands can’t benefit from each others system-wide node cache