The Dockerfile should only be responsible for building the runtime image
Cache node_modules instead of installing every time; reuse it when package.json hasn’t changed
Use npm prune --production to remove devDependencies, which can cut the image size by more than half
Use slim base images wherever possible, such as node:20-slim. If you don’t even need npm (i.e. nothing in the Dockerfile uses npm), you can go with the even smaller node:20-alpine