Docker Interview Questions — Practice with Real Questions
Reviewed by Mark Dickie · Last updated
Docker is a platform that packages applications and their dependencies into lightweight, portable containers so they run consistently across environments. For interviews, you need to understand the core build–run–ship lifecycle: how to write a Dockerfile, how layers and caching work, how to manage container networking and persistent volumes, and how Docker differs from virtual machines. You should also be comfortable with multi-stage builds, image size optimization, and the basics of Docker Compose, since many teams use it to orchestrate multi-container development environments.
What does a Docker interview typically test?
Interviews tend to split across these areas, with image authoring and runtime troubleshooting carrying the most weight:
| Area | What gets asked |
|---|---|
| Dockerfile authorship | Layer ordering, caching, multi-stage builds, choosing a base image |
| Containers & runtime | Lifecycle commands, exec vs attach, resource limits, restart policies |
| Networking | Bridge vs host vs overlay, port publishing, DNS between containers |
| Volumes & persistence | Named vs bind mounts, volume drivers, backup strategies |
| Image management | Tagging, layer inspection, pruning, registry workflow |
| Compose & basics | Service definitions, depends_on, environment injection |
How should you prepare for a Docker interview?
- Write and build at least one non-trivial Dockerfile from scratch, then reduce its final image size using a multi-stage build.
- Run a two-container setup with Docker Compose where one service depends on another, and verify DNS resolution and volume sharing between them.
- Inspect a running container with
docker inspectand read the layer history withdocker historyto understand how images stack. - Practice diagnosing common failures: a container that exits immediately, a port conflict, a volume permission error, or an image that works locally but fails in CI because the base tag drifted.
- Review the differences between Docker and a full virtual machine, and be ready to explain why containers share the host kernel and what that means for isolation.
The quiz below pulls from real interview questions across these areas, so you can check which topics need another pass before you sit down with an interviewer.
Key facts
- Tarmac has 104 Docker interview questions on this topic, 25 of them on this page, at difficulty 1–5 of 5.
- Tarmac tracked 4,024 job postings asking for Docker in August 2026.
- Roles asking for Docker advertise a median base salary of £80,000, across 654 job postings as of August 2026.
- Tarmac last reviewed these Docker interview questions on 31 August 2026.
At a glance
| Questions | 25 shown · 104 in the bank |
|---|---|
| Difficulty | 1–5 of 5 |
| Formats | Multiple choice, Flashcard, Fill in the blank, Short answer, Multiple answer, Ordering, True / false, Code output, Find the bug, Design exercise |
What you'll review
- dockerfile
- container lifecycle
- layer caching
- secrets handling
- port publishing
- healthcheck
- image size
- compose depends on
- container dns
- multi stage builds
- volumes
- restart policies
- entrypoint cmd
- run vs exec
- non root user
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
Docker/docker-images/dockerfile
You need to copy a local requirements.txt from the build context into the image. Docker's own best-practice guidance says to prefer one instruction here. Which, and why?#
Options
Show answer
Prefer COPY for copying local files into an image. COPY only copies files and directories from the build context, so its behaviour is transparent and predictable. ADD does extra implicit work — it can fetch remote URLs and auto-extracts local tar archives — which makes builds surprising. Both preserve permissions and handle single files, so reserve ADD for the narrow case where you actually want local-archive extraction.
Docker's Dockerfile best practices recommend COPY for plain file copies because its behaviour is transparent: it only copies local files/directories from the build context. ADD does extra implicit work — it can download remote URLs and will auto-extract local tar archives — which makes builds less predictable and can mask surprises. Both preserve permissions and both handle single files. Reaching for ADD 'just in case' is the misconception; use ADD only when you specifically want local-archive auto-extraction.
Docker/docker-containers/container-lifecycle
What is the difference between a Docker image and a Docker container?#
Show answer
An image is a read-only template: a stack of immutable layers holding the filesystem, dependencies, and default config built from a Dockerfile. A container is a running (or stopped) instance of an image — Docker adds a thin writable layer on top of the image's read-only layers and runs the entrypoint process. One image can spawn many independent containers, each with its own writable layer. The image is the class; the container is the object instance.
The image/container distinction is the most fundamental Docker concept. The image is the immutable build artifact; a container is a live instance with its own writable layer and process. Grasping that many containers share one image's read-only layers explains both Docker's storage efficiency and why container-local writes vanish when the container is removed.
Docker/docker-best-practices/layer-caching
In a Dockerfile, each instruction (such as RUN, COPY, or ADD) creates a new cached _____ that Docker reuses on subsequent builds only when the instruction itself and all of its input files are unchanged.#
Show answer
In a Dockerfile, each instruction (such as RUN, COPY, or ADD) creates a new cached layer that Docker reuses on subsequent builds only when the instruction itself and all of its input files are unchanged.
Docker builds images as a stack of read-only layers, one per Dockerfile instruction. Each layer is cached; if the instruction and its inputs (e.g., files being COPYed) have not changed since the last build, Docker reuses the cached layer instead of re-executing the instruction.
Docker/docker-best-practices/secrets-handling
When following Docker best practices for secrets handling, what built-in swarm feature should you use to securely provide sensitive data (like API keys or passwords) to a container at runtime instead of embedding them in the image or passing them as plain-text environment variables?#
Show answer
Docker Secrets (via docker secret / Swarm secrets). Secrets are stored in the Swarm Raft log, delivered to the container over a secure TLS mutual connection, and mounted as a file under /run/secrets/<secret_name> inside the container.
Docker Secrets is the built-in mechanism for securely managing sensitive data in Docker Swarm. Secrets are encrypted at rest in the Raft log, transmitted over mutual TLS, and mounted as temporary files (under /run/secrets) that are never written to disk in the image or exposed in plain-text environment variables — making them the best-practice choice over baking secrets into images or using -e flags.
Docker/docker-networking/port-publishing
Your Dockerfile has EXPOSE 8080. You run the container with docker run myimg (no -p). Can a client on the host reach the app on port 8080?#
Options
Show answer
No. EXPOSE is only metadata — it records which port the application listens on but never publishes it. For host traffic to reach the container you must publish the port explicitly with -p 8080:8080, or auto-publish all exposed ports with -P. Publishing with -p even works without an EXPOSE line. Assuming EXPOSE opens the port is a common cause of an unreachable container.
EXPOSE is documentation/metadata: it records which port the app listens on but never publishes anything. To let host traffic reach the container you publish the port with -p 8080:8080 (or auto-publish all exposed ports with -P). You don't even need EXPOSE to publish — -p works regardless. The trap is assuming EXPOSE opens the port; teams hit this when a 'working' image is unreachable in production because nobody published the port.
Docker/docker-best-practices/healthcheck
Which of the following are recognized best practices when configuring a Docker HEALTHCHECK instruction? (Select all that apply.)#
Options
Pick every one that applies.
Show answer
The best practices are: exit code 0 for healthy and non-zero for unhealthy, use --start-period to give slow-starting apps a grace period, and keep the health check command fast so it finishes well under --timeout. You should NOT use HEALTHCHECK to replace orchestrator-level liveness/readiness probes, and a hanging command is never preferable to a quick non-zero exit.
Docker's HEALTHCHECK contract requires the command to exit 0 for healthy and non-zero (typically 1) for unhealthy, so (a) is correct. The --start-period flag defines a grace period during which failures don't count toward retries, which is a best practice for slow-starting apps, so (b) is correct. Health check commands should be fast and well within --timeout to avoid false negatives and wasted resources, so (c) is correct. Option (d) is wrong because orchestrator probes offer finer-grained control (separate liveness vs. readiness) and should complement, not be replaced by, Docker's health check. Option (e) is wrong because a hanging command eventually hits --timeout and is marked unhealthy anyway; a fast, deterministic non-zero exit is always preferable.
Docker/docker-best-practices/image-size
What is a multi-stage build in Docker and how does it help reduce the final image size?#
Show answer
Use a multi-stage build: compile/build your application in a full SDK base image in the first stage, then COPY only the resulting binary/artifacts into a minimal runtime base image (e.g. alpine, scratch, or a slim distroless image) declared in the final stage. This excludes build tools, intermediate files, and dev dependencies from the final image without changing your build process. Pair this with a minimal base image and a well-ordered Dockerfile (copy dependency manifests before source code) to maximize layer cache reuse.
A multi-stage build lets you use a heavy base image with compilers and SDKs for the build stage, then copy only the produced artifacts into a small runtime image. The final image never contains the build tools or intermediate dependencies, dramatically reducing its size. This is the most fundamental and widely recommended practice for minimizing Docker image size.
Docker/docker-best-practices/layer-caching
In a Dockerfile, the RUN instruction that installs dependencies (npm install / pip install -r requirements.txt) should be placed in a layer _____ the layer that copies the application source code. This ordering ensures that the dependency-install layer is reused from cache when only the application source changes, because Docker builds each layer only when an instruction or its inputs have _____ since the last build.#
Show answer
In a Dockerfile, the RUN instruction that installs dependencies (npm install / pip install -r requirements.txt) should be placed in a layer before the layer that copies the application source code. This ordering ensures that the dependency-install layer is reused from cache when only the application source changes, because Docker builds each layer only when an instruction or its inputs have changed since the last build.
Docker layer caching works top-down: each instruction produces a layer, and that layer is reused from cache only if the instruction text and all preceding layers are identical to the previous build. If you copy application source code before installing dependencies, any source change invalidates the cache for the COPY layer and every layer after it — including the dependency-install step, forcing a wasteful re-download. By placing the dependency-install instruction (and the narrow COPY of just the lockfile / requirements file it needs) before the broad COPY of application source, the dependency layer stays cached across source-only changes. Docker then only rebuilds the final COPY and subsequent layers.
Docker/docker-compose/compose-depends-on
In a docker-compose.yml, the services form a linear dependency chain: web lists depends_on: [api], api lists depends_on: [cache], and cache lists depends_on: [db]. No condition key is specified on any dependency. Place the four services in the order Docker Compose starts them when you run docker compose up — from first started to last started.#
Put these in order
Show answer
Docker Compose starts services in dependency order: db first, then cache, then api, then web. The depends_on directive ensures each declared dependency is started before the dependent service, so a linear chain produces a strict bottom-up startup sequence.
depends_on makes Compose start each dependency before the service that declares it. cache cannot start until db is up, api cannot start until cache is up, and web cannot start until api is up. Because the chain is linear with no parallel branches, the full startup order is db → cache → api → web.
Docker/docker-networking/container-dns
On Docker's default bridge network, one container can reach another by its container name (e.g. ping db) out of the box.#
Options
Show answer
False. The default bridge network provides no automatic DNS resolution — containers on it can reach each other only by IP address. Name-based resolution (e.g. reaching a container called db by the name db) is a feature of user-defined bridge networks: create one with docker network create, attach both containers, and they resolve each other by name. This is why Docker Compose puts services on a user-defined network by default.
False. The default bridge network does NOT provide automatic DNS resolution between containers — they can only reach each other by IP address (the legacy --link flag aside). Name-based resolution is a feature of user-defined bridge networks: create one with docker network create mynet, attach both containers, and they can resolve each other by container name. This is the single biggest reason to use a user-defined network (and why Docker Compose creates one for you), and a classic 'why can't my app find the database' bug.
Docker/docker-images/multi-stage-builds
Explain how a multi-stage build produces a smaller final image, and what COPY --from does in that flow.#
Show answer
A multi-stage build uses several FROM instructions, each starting a new stage. An early 'build' stage uses a heavy base image with compilers, dev dependencies, and build tools to compile or bundle the app. The final stage starts from a slim runtime base and uses COPY --from=build /path/to/artifact . to pull only the built artifacts out of the earlier stage. Everything in the build stage — the toolchain, intermediate files, dev dependencies — is left behind and never ships in the final image, so it's far smaller and has a smaller attack surface. Naming the stage with AS build keeps the COPY --from reference stable even if stages are reordered.
The win is separating the build environment from the runtime environment. The build stage carries the compiler/SDK and dev dependencies; the final slim stage receives only the compiled output via COPY --from. Because each stage is independent, the bulky toolchain never lands in the shipped image — smaller pulls, faster deploys, fewer CVEs. Answers that just say 'use a smaller base image' miss the mechanism: it's discarding the build-only layers, not merely picking alpine.
Docker/docker-storage/volumes
Contrast a named volume with a bind mount for persisting container data, and give one situation where each is the right choice.#
Show answer
A named volume is storage managed by Docker in its own area on the host; you reference it by name and Docker owns its lifecycle, so it's portable, easy to back up, and decoupled from any host path. A bind mount maps a specific host directory straight into the container, so its contents depend on the host's filesystem layout and the data is whatever is already at that path. Use a named volume for production state like a database's data directory — it survives container removal and isn't tied to a host path. Use a bind mount in development to mount your source code into the container so edits on the host show up live inside it. Both persist data beyond the container's life; the difference is who manages the storage and how coupled it is to the host.
The crisp split: a named volume is Docker-managed storage (portable, backup-friendly, host-path-agnostic); a bind mount is a direct window onto a host directory (tied to the host layout, whatever's already there). Production data (DB files) → named volume. Live-editing source during development → bind mount. The key insight is that both persist data outside the container lifecycle — the difference is management and host coupling, not 'one persists and one doesn't'.
Docker/docker-containers/container-lifecycle
Order the steps from a Dockerfile on disk to a running container serving traffic on the host.#
Put these in order
Show answer
Going from a Dockerfile to a running, reachable container follows a fixed sequence. The correct order is: first docker build reads the Dockerfile and produces an image of read-only layers, then the image is tagged so it can be referenced by name, then docker run creates a container by adding a writable layer on top of the image, then the container's entrypoint process starts and the app begins listening, and finally Docker forwards the published host port to the container port so external clients can reach the app.
Build comes first: the Dockerfile is turned into an immutable image of read-only layers. Tagging gives that image a referenceable name. docker run then instantiates a container — a writable layer plus runtime config — from the image. The entrypoint process launches and the app starts listening on its container port. Finally the -p mapping makes Docker forward the host port to the container port so outside traffic arrives. Knowing this order is what lets you reason about where something broke: a build failure, a missing tag, a crash on start, or an unpublished port.
Docker/docker-containers/restart-policies
To make a container restart automatically if it crashes AND come back up after a daemon/host reboot, but NOT restart if you manually stopped it, use docker run --restart _____ ....#
Show answer
To make a container restart automatically if it crashes AND come back up after a daemon/host reboot, but NOT restart if you manually stopped it, use docker run --restart **unless-stopped** ....
unless-stopped restarts the container on failure and after a Docker daemon restart, but it stays down if you explicitly docker stop it. The neighbouring policy always differs in exactly one way: it will restart even a manually-stopped container when the daemon comes back. on-failure only restarts on a non-zero exit and does not survive a daemon restart, and no (the default) never restarts. Picking the wrong policy is why a container you deliberately stopped keeps coming back, or why a crashed one stays down.
Docker/docker-best-practices/healthcheck
In Docker Engine (standalone, no orchestrator), when a container's health status transitions to "unhealthy" because the HEALTHCHECK failed --retries consecutive times, the Docker Engine automatically stops and restarts that container.#
Options
Show answer
False. Docker Engine does not stop or restart a container when it becomes "unhealthy"; the container continues running with the unhealthy status. Automatic restart on health failure requires an orchestrator such as Docker Swarm, which treats unhealthy service tasks as failed and reschedules them.
Docker Engine itself never stops or restarts a container just because its health status becomes "unhealthy." The container keeps running with the "unhealthy" label. Only an external orchestrator — such as Docker Swarm, which restarts unhealthy service tasks — takes action based on health status. A standalone docker run container will remain running indefinitely in the unhealthy state.
Docker/docker-containers/entrypoint-cmd
Why does Docker recommend the exec form ENTRYPOINT ["node", "server.js"] over the shell form ENTRYPOINT node server.js for a long-running service?#
Options
Show answer
Exec form launches your process directly so it becomes PID 1 and receives signals — including the SIGTERM that docker stop sends — letting it shut down gracefully. Shell form runs the command through /bin/sh -c, so the shell becomes PID 1 and often fails to forward SIGTERM to your process; docker stop then waits the grace period and SIGKILLs it. That is the usual cause of containers that take ten seconds to stop.
With exec form, your process is launched directly and becomes PID 1, so it receives signals (notably the SIGTERM docker stop sends) and can drain connections and exit cleanly. Shell form runs /bin/sh -c "node server.js", making the shell PID 1; the shell frequently doesn't forward SIGTERM to its child, so docker stop hangs for 10s and then SIGKILLs — abrupt termination, dropped requests. Shell form does pass args and isn't slower; it just breaks signal handling. This is the classic 'my container takes 10 seconds to stop' bug.
Docker/docker-best-practices/secrets-handling
You need a private registry token available only while installing dependencies during the build, and it must NOT end up readable in the final image. Select every approach that leaks the token into the image's layers or history.#
Options
Pick every one that applies.
Show answer
ARG and ENV values are baked into image metadata and recoverable with docker history --no-trunc, and ENV also persists into every container's environment. Writing a secret then rm-ing it later does not help either: layers are immutable and retained, so the file survives in the earlier layer. The only safe approach is a BuildKit secret mount (RUN --mount=type=secret), which exposes the value to one RUN step and never writes it to any layer.
ARG and ENV values are baked into image metadata and are recoverable with docker history --no-trunc (and ENV also persists into every running container's environment). Writing the secret then rm-ing it in a later layer doesn't help: each layer is immutable and retained, so the earlier layer still contains the file even though the final filesystem hides it — anyone can extract it from the layer. The only safe option is a BuildKit secret mount (RUN --mount=type=secret), which exposes the secret to a single RUN and never writes it to any layer. Believing a later rm 'deletes' a secret is the most dangerous misconception here.
Docker/docker-containers/entrypoint-cmd
Given this image, what does docker run myimg world print? (Both instructions are exec form.)#
FROM alpine:3.20
ENTRYPOINT ["echo", "hello"]
CMD ["docker"]Options
Show answer
hello world
With an exec-form ENTRYPOINT, arguments passed to docker run are appended after the entrypoint and they replace CMD entirely. So CMD ["docker"] is the default argument used only when you run with no args (giving echo hello docker), but here world overrides it. The container runs echo hello world, printing hello world. It is not hello docker world (the run arg replaces CMD, it doesn't add to it) and not just world (the entrypoint and its hello argument are fixed). This entrypoint-fixed, CMD-as-default-args pattern is the standard way to build a configurable container command.
Docker/docker-best-practices/layer-caching
Every time a developer changes a single source file, this Dockerfile re-runs npm install from scratch and the build is slow. Which line is the root cause of the wasted cache?#
1| FROM node:22-slim
2| WORKDIR /app
3| COPY . .
4| RUN npm install
5| CMD ["node", "server.js"]Options
Show answer
Line 3 — COPY . . copies all source before npm install, so any source change invalidates that layer and forces the install to re-run; copy package*.json and install first, then copy the rest
Docker caches layers in order and invalidates a layer (and everything after it) when its inputs change. COPY . . hashes the whole build context, so editing any source file busts that layer — and the RUN npm install on the next line, which depends on it, re-runs every time. The fix is dependency-aware ordering: COPY package*.json ./, then RUN npm install, then COPY . .. Now npm install's layer only rebuilds when the manifest changes, not on every source edit. npm ci vs install, slim vs full base, and WORKDIR are all irrelevant to the cache miss. This ordering mistake is the most common cause of slow Docker builds in CI.
Docker/docker-best-practices/healthcheck
In a Docker HEALTHCHECK, failures that occur during the --start-period grace window do not count toward the --retries limit that determines when the container is marked unhealthy.#
Options
Show answer
True. During the --start-period grace window, health-check failures do not count toward the --retries threshold, so slow-starting containers are not prematurely marked unhealthy. Once the start period elapses, consecutive failures resume counting normally. This makes --start-period essential for containers with long bootstrap times.
Docker's HEALTHCHECK directive supports a --start-period (default 0s) that acts as a bootstrap grace window. Health-check failures that happen within this interval are not tallied toward --retries, so a slow-to-start service won't be prematurely marked unhealthy. Once --start-period elapses, the normal counting of consecutive failures resumes, and the container is declared unhealthy when the --retries threshold of consecutive failures is reached. This is documented Docker behavior and is a key best-practice lever for long-boot containers.
Docker/docker-best-practices/image-size
You are refactoring a monolithic Dockerfile into a multi-stage build for a production Node.js API. The following five instructions belong in the final (runtime) stage. Order them as they should appear in the Dockerfile — base image first, entrypoint last — so that the stage is cache-friendly (least-frequently-changing layers first) and contains only runtime artifacts.#
Put these in order
Show answer
The correct order is: FROM node:20-alpine, WORKDIR /app, COPY --from=deps /app/node_modules ./node_modules, COPY --from=builder /app/dist ./dist, CMD ["node", "dist/server.js"]. FROM must come first to define the base image; WORKDIR must precede COPY so relative destinations resolve correctly; production dependencies are copied before application code because they change less frequently, maximizing layer-cache hits; and CMD is placed last by convention as a metadata-only instruction.
FROM node:20-alpine (b) must come first — it defines the base image and the starting layer of the final stage. WORKDIR /app (d) must precede every COPY instruction because the COPY destinations use relative paths (./node_modules, ./dist) that resolve against the working directory; without WORKDIR they would land in / instead of /app. COPY --from=deps /app/node_modules (a) comes before COPY --from=builder /app/dist (c) because production dependencies change only when package.json changes, while compiled application code changes on every source-code edit. Placing the less-frequently-changing dependency layer earlier maximizes build-cache hits: when application code changes, the dependency layer cache remains valid and Docker does not need to re-copy or rebuild it. CMD (e) is placed last by convention — it is a metadata instruction that does not create a filesystem layer and serves as the stage's runtime entrypoint, so it logically concludes the stage definition. This ordering produces a final image containing only the minimal Alpine base, production dependencies, compiled artifacts, and the start command — no build tools, source files, or dev dependencies — which is the core image-size best practice enabled by multi-stage builds.
Docker/docker-containers/run-vs-exec
An image built from the following Dockerfile is running as a container:#
Options
Pick every one that applies.
Show answer
Statements a, b, and c are true. docker exec bypasses the image ENTRYPOINT and runs the specified command directly; it requires the container to be in running status; and -e environment variables are scoped to the exec process only, not PID 1. Statement d is false because docker exec inherits the image's USER directive, so the process runs as appuser unless overridden with -u.
docker exec runs a new process inside an already-running container and does not re-run the image's ENTRYPOINT — it executes the specified command directly, so (a) is true. It also requires the container to be in running state; a container that was only created (never started) will produce an error, making (b) true. Environment variables set via docker exec -e are scoped to the exec session's process tree only; the original PID 1 process is unaffected, so (c) is true. Option (d) is false: docker exec inherits the USER directive from the image, so the exec process also runs as appuser unless explicitly overridden with -u.
Docker/docker-best-practices/image-size
Design a production-grade Dockerfile for a Node.js HTTP service. A junior wrote a single-stage image: FROM node:22, COPY . ., RUN npm install, CMD npm start. It works but is ~1.1 GB, rebuilds dependencies on every code change, runs as root, and docker stop takes 10 seconds.#
Show answer
Structure. Two stages. Build stage FROM node:22 AS build: set WORKDIR, COPY package*.json ./, RUN npm ci, then COPY . . and RUN npm run build. Runtime stage FROM node:22-slim (or distroless/node): copy only what's needed.
Caching. Copy the manifest and lockfile and run npm ci before copying source, so the dependency layer is cached and only rebuilds when package.json/lock changes — not on every code edit. A .dockerignore (node_modules, .git, tests) keeps the context small and the cache stable.
Slim runtime. In the final stage, COPY --from=build --chown=node:node /app/dist ./dist and either copy a production-only node_modules (npm ci --omit=dev in the build stage, copied over) or reinstall prod deps. The compiler/dev deps stay in the build stage and never ship — image drops from ~1.1 GB to a couple hundred MB.
Non-root. The official node images ship a node user; USER node before CMD (with --chown=node:node on copies so it can read its files). If rolling my own: RUN useradd ... then USER.
Signals. CMD ["node", "dist/server.js"] (exec form) so Node is PID 1 and gets SIGTERM directly; the app handles SIGTERM to stop accepting connections and drain in-flight requests. CMD npm start runs npm (and a shell), which becomes PID 1 and doesn't forward SIGTERM — so docker stop waits the 10s grace period then SIGKILLs. Adding --init/tini is an alternative for zombie reaping.
Secrets. npm ci --omit=dev for reproducible, dev-free installs. A private registry token goes through a BuildKit secret: RUN --mount=type=secret,id=npmtoken ..., never ARG/ENV (those are recoverable via docker history).
Tradeoffs. Alpine is smallest but musl can break native modules; slim/distroless trade a little size for glibc compatibility and (distroless) harder debugging. Pin base tags rather than latest for reproducibility. A HEALTHCHECK lets the orchestrator detect a wedged process.
A strong answer maps each goal to a concrete mechanism: multi-stage + slim base for size, manifest-first copy ordering for cache reuse, a USER line for non-root, exec-form CMD (or an init) for SIGTERM-based graceful shutdown, and a BuildKit secret mount instead of ARG/ENV. The highest-signal points are understanding why CMD npm start breaks signal forwarding (shell/npm becomes PID 1) and why copying the lockfile before the source is what makes CI rebuilds fast. Reciting 'use alpine' without the cache-ordering and PID-1 reasoning is the shallow answer.
Docker/docker-best-practices/layer-caching
You are given a monorepo with a Python FastAPI backend (~80 dependencies in requirements.txt, code split across 15 modules) and a React frontend (managed with Yarn workspaces, ~300 transitive deps). The single Dockerfile currently does a COPY . . right after the base image, then runs pip install -r requirements.txt and yarn install && yarn build in the same stage, then bundles everything into one final image. The CI pipeline rebuilds this image on every push and the average build takes 12 minutes, with cache misses on nearly every run.#
Show answer
Below is a revised Dockerfile skeleton followed by the rationale for each decision.
# syntax=docker/dockerfile:1.7
# ---- Stage 1: Python deps ----
FROM python:3.12-slim AS py-deps
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt
# ---- Stage 2: JS deps + build ----
FROM node:20-slim AS js-build
WORKDIR /app
# Copy only manifest files for root + all workspace packages
COPY package.json yarn.lock ./
COPY backend/package.json backend/
COPY frontend/package.json frontend/
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
yarn install --frozen-lockfile
# Now copy source and build
COPY . .
RUN yarn workspace frontend build
# ---- Stage 3: Final runtime ----
FROM python:3.12-slim AS runtime
WORKDIR /app
# Copy installed Python packages from deps stage
COPY --from=py-deps /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
# Copy built JS assets
COPY --from=js-build /app/frontend/dist ./frontend/dist
# Copy application source last (changes most frequently)
COPY backend ./backend
EXPOSE 8000
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
COPY ordering rationale: Docker builds each layer as a cache-keyed hash of the parent layer ID plus the instruction and the checksums of all files referenced by COPY. If COPY . . precedes pip install, any change to any source file (even a comment in a React component) invalidates that layer and every layer below it, including the expensive pip install. By copying only requirements.txt first, the pip layer stays cached as long as the lock file is unchanged — which is the common case for source-only PRs. The same logic applies to the JS stage: package.json and yarn.lock are copied before yarn install.
Monorepo workspace manifests: Because Yarn workspaces resolve dependencies across multiple package.json files, we must copy every workspace package.json (at its correct relative path) before running yarn install --frozen-lockfile. We use a targeted set of COPY instructions rather than COPY . . so that source changes in frontend/src/ do not invalidate the dependency layer. The --frozen-lockfile flag ensures the lock file is authoritative and the install fails fast if manifests and lock are out of sync.
Multi-stage build: The final runtime image is based on python:3.12-slim (not the node image) and contains only the installed site-packages, the pre-built static JS assets, and the backend source. This drops the entire Node.js toolchain, dev dependencies, and intermediate build files from the final image, reducing it from potentially 1.2 GB to ~200 MB. Artifacts that cross stage boundaries are: site-packages (from py-deps), frontend/dist (from js-build), and backend source (copied directly in the final stage).
BuildKit cache mounts: --mount=type=cache,target=/root/.cache/pip persists the pip download cache across builds so that even when the requirements layer is invalidated, pip does not re-download packages from PyPI — it reuses cached wheels. Similarly, the yarn cache mount avoids re-downloading tarballs. Risks: (1) if the Python minor version in the base image changes (e.g., 3.12.1 → 3.12.2), cached wheels compiled against the old ABI may be reused and cause subtle import failures; (2) on ephemeral CI runners, the cache is lost unless a distributed cache backend (e.g., --cache-to=type=registry or a BuildKit remote cache) is configured; (3) stale cache entries can mask dependency resolution issues if the lock file is regenerated without a clean install. The cache mount should be paired with --frozen-lockfile / lock-file-based installs to mitigate drift.
.dockerignore: Critical entries include node_modules, .git, __pycache__, *.pyc, venv, .venv, frontend/dist, frontend/node_modules, .env, .env.*, *.log, .pytest_cache, .mypy_cache, and IDE directories (.vscode, .idea). These prevent local build artifacts and secrets from entering the build context, reduce the context tarball sent to the daemon (which alone can save minutes when node_modules is large), and ensure that local node_modules or virtualenvs do not shadow the container-installed dependencies when COPY . . runs later in the build stage.
This design exercise tests staff-level knowledge of Docker layer caching: the candidate must reason about cache-key invalidation chains, dependency-layer isolation in a monorepo, multi-stage artifact boundaries, BuildKit cache-mount semantics and risks, and build-context minimization — all interconnected in one realistic scenario.
Docker/docker-best-practices/non-root-user
When you specify USER 65532:65532 in a Dockerfile before a COPY --from=builder /app /app instruction, what permission pitfall can arise, and how do you correctly ensure the non-root user owns those copied files?#
Show answer
USER only affects RUN (and CMD/ENTRYPOINT at runtime) — it does NOT change the ownership of files copied by COPY. COPY --from=builder /app /app preserves the ownership from the builder stage; if those files are owned by root there, they arrive owned by root in the final image regardless of USER 65532:65532, and the non-root process cannot write to any subdirectory it needs to modify. Fix: add --chown to the COPY itself, e.g. COPY --chown=65532:65532 --from=builder /app /app, which sets ownership at copy time without requiring an extra RUN chown layer. Alternatively, chown the files inside the builder stage before copying.
USER changes the effective user for subsequent RUN instructions but does not affect COPY file ownership. COPY --from=<stage> preserves the source stage's ownership, so files owned by root in the builder stage remain root-owned in the final image even when USER 65532:65532 is set before the copy. Adding --chown=<uid>:<gid> to the COPY instruction sets ownership at copy time, which is the correct fix because it requires no extra layer and works regardless of the source stage's ownership.
Related interview questions
Job market
See docker salaries and hiring demand from live job postings.
The other 79 questions
This page shows 25 and marks what you pick. That's as far as a page can go. A free account opens the other 79 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan