In one stage we --mount=type=bind,source=package.json but in another stage we simply copy it

Hello,
When I use docker init on my NodeJS project it creates a Dockerfile which looks like this

# syntax=docker/dockerfile:1

ARG NODE_VERSION=20.11.0

FROM node:${NODE_VERSION}-alpine as base

WORKDIR /usr/src/app

FROM base as deps
RUN --mount=type=bind,source=package.json,target=package.json \
    --mount=type=bind,source=package-lock.json,target=package-lock.json \
    --mount=type=cache,target=/root/.npm \
    npm ci --omit=dev

FROM deps as build
RUN --mount=type=bind,source=package.json,target=package.json \
    --mount=type=bind,source=package-lock.json,target=package-lock.json \
    --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .

RUN npm run build

FROM base as final

ENV NODE_ENV=production

USER node

COPY package.json .
COPY --from=deps /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/dist ./dist

EXPOSE 3000

CMD npm run start:prod

Why in deps and build stages it suggests to --mount=type=bind package.json, but in final stage it just COPY it? Does it have some meaning or it’s done simply for illustrative purposes? In my opinion in both cases we can use ‘COPY’ to put package.json in the image

I wouldn’t say this init result is perfct, but I am not a nodejs developer either. Here is my opinion.

Every RUN or COPY instruction creates a new container that can be saved as a new layer. Not creating a new container but only mounting the file could be a little faster. It is like running a container and mounting al the necessary folders and files and run a command in a single step.

In the final stage, you may want to have the package json in the image, so when someone uses the image, it is lcear what the dependencies are and that single json file will not increase the size of the image a lot. By that time, the application is already built, so it is just making sure the dependency list file is shared.

It doesn’t help much now, but you can optimize your images if you copy files only when you need those in the image.

This part is the strange one to me. Usualy we define CMD this way:

CMD ["npm", "run", "start:prod"]

So if npm handles stop signals correctly, you can stop the container without waiting for the timeout (10 seconds) and forcing the process to stop without letting it to finish some necessary processes.

The original syttax used the SHELL FORM, so a shell is started in the container in which npm runs, but the shell will not forward the stop signal to npm by default.