Rewrite this Node Dockerfile to apply container best practices.
The Dockerfile below builds a Node service but ignores best practices: an unpinned base, no .dockerignore, source copied before the dependency install so every code edit rebuilds npm install, and it runs as root.
Constraints:
- Pin the base image to a specific minor tag (a slim variant).
- Order layers so a source-only change does NOT re-run
npm install. - Install only production dependencies.
- Run the container as a non-root user.
FROM node
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
Rewrite the Dockerfile (and note the .dockerignore) to satisfy the constraints.
Pin the base (node:20.11-slim), then COPY package*.json and run npm ci --omit=dev before COPY . ., so a source-only edit reuses the cached dependency layer. Add a .dockerignore for node_modules/.git to keep the context small, and finish with USER node to run unprivileged.
- ✗Copying source before the manifest, so npm install re-runs on every code edit
- ✗Leaving the base on the floating
node/latesttag instead of pinning it - ✗Running as root and shipping node_modules into the context with no .dockerignore
- →Why is
npm cipreferable tonpm installin a reproducible build? - →What does pinning the base image by digest add beyond pinning by tag?
Layer order is the main lever: copy the manifest and install dependencies before copying the source, so a code edit reuses the cache. The base is pinned to a slim tag, only production dependencies are installed, and USER node drops the process off root. .dockerignore keeps the context small.
FROM node:20.11-slim
WORKDIR /app
# Dependencies before source: this layer survives source edits.
COPY package*.json ./
RUN npm ci --omit=dev
# Copy the source last.
COPY . .
# The non-root user ships with the node base image.
USER node
CMD ["node", "server.js"]
.dockerignore:
node_modules
.git
npm-debug.log