Question presented to candidate: "Your Node.js app's Docker image is 1.2GB, and a teammate says that's a genuine security and deploy-speed problem, not just an aesthetic one. What specifically is bloating that image, and what technique actually removes it?"
What a strong answer should cover:
- A "naive" single-stage
Dockerfile(oneFROM node,COPY . .,RUN npm install) ships the full Node.js build toolchain, the complete OS userland of the base image, AND everydevDependency(TypeScript, ESLint, test frameworks, build tools) — none of which the running application actually needs, only what was needed to build it. - 📌 Verified, not assumed: installing with
devDependenciesincluded versusnpm install --omit=dev(the real mechanism a multi-stage build's final layer exploits) produced a real, measurednode_modulessize of 74MB versus 4.3MB — a genuine ~94% reduction from dropping only the dev toolchain, the concrete, real proof of exactly which artifacts a naive single-stage build ships that a multi-stage one does not. - 📌 Interview term: multi-stage build — a
Dockerfilewith more than oneFROMinstruction. An early builder stage has the full toolchain (installs all dependencies, compiles TypeScript if used, runs the build) — the final stage starts fresh from a minimal base image andCOPY --from=builderpulls in ONLY the specific built artifacts (compiled output, andnode_modulesreinstalled or copied with--omit=dev) actually needed to run, discarding the builder stage's toolchain entirely from the shipped image. - 📌 Interview term: distroless — a base image (Google's
gcr.io/distroless/nodejsfamily) containing only the Node.js runtime and its minimal OS dependencies, deliberately without a shell, package manager, or general-purpose OS userland at all. This shrinks the image further than a general-purpose slim image AND meaningfully reduces attack surface — an attacker who achieves code execution inside the container has no shell to pivot with, no package manager to install additional tools, genuinely fewer avenues to escalate. - The honest trade-off, stated precisely: distroless images are harder to debug (no shell means
docker exec -it ... shfor interactive debugging genuinely does not work) — a common middle ground is a-slimvariant (a minimal but not fully distroless base, still has a shell) for easier debugging, reserving true distroless for production images where the security/size benefit outweighs the debugging convenience.
Clarifying questions expected:
- "Does the build step involve compiling (TypeScript, a bundler), or is this plain JavaScript with no build step?" — decides whether the builder stage needs a compile step at all.
- "Is interactive shell debugging inside the running container a real operational need, or is all debugging done via logs/metrics?" — directly decides distroless vs. a slim base with a shell.
Code / implementation expected: Yes — a full multi-stage Dockerfile, with the production-only-dependency mechanism directly measured in this sandbox as the concrete proof of what the technique actually removes from the final image.