The 60 Docker questions interviewers actually ask, with direct answers, runnable commands, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
Docker is an open platform for building, shipping, and running applications inside containers. A container bundles your code with its libraries and runtime into one artifact, so the same image runs the same way on a laptop, a CI runner, and production. According to Docker's official overview, containers use OS-level isolation to run as isolated processes on the host, which is why they're lighter and start faster than virtual machines that each boot a full guest kernel. In interviews, Docker questions probe the mental model behind that (images versus containers, the layered filesystem, networking, volumes, and the difference between the build and run phases) rather than trivia. This page collects the 60 questions that come up most, each with a direct answer and runnable commands. If the round runs as a live AI coding interview, pair this bank with our AI interview preparation guides for the format side.
Watch: Learn Docker in 7 Easy Steps: Full Beginner's Tutorial
Video: Learn Docker in 7 Easy Steps: Full Beginner's Tutorial (Fireship, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Docker certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Docker is a platform that packages an application with its dependencies into a container that runs the same way on any machine. It solves the classic 'works on my machine' problem by shipping the app and its environment together as one artifact.
You define the environment once in a Dockerfile, build an image, and run it anywhere with a container runtime. That gives you consistency across a developer laptop, CI, and production without manual setup on each host.
Key point: A one-line definition plus the 'works on my machine' framing beats a feature list. Interviewers open with this to hear how you organize an answer.
Watch a deeper explanation
Video: Docker in 100 Seconds (Fireship, YouTube)
An image is a read-only template that holds your app, its dependencies, and instructions to run it. A container is a running instance of an image, with its own writable layer on top.
The class-versus-object analogy helps: one image can spawn many containers, and each container's changes stay isolated in its writable layer, gone once the container is removed unless you persist them.
docker build -t myapp:1.0 . # produce an image
docker run -d myapp:1.0 # start a container from it
docker run -d myapp:1.0 # a second, independent container| Image | Container | |
|---|---|---|
| State | Read-only template | Running instance with a writable layer |
| Created by | docker build | docker run |
| Count | One image | Many containers from it |
| Lifecycle | Persistent until removed | Ephemeral by default |
Key point: The follow-up is almost always 'where does data written inside the container go?'. Have the writable-layer answer ready.
A Dockerfile is a plain text file with step-by-step instructions Docker follows to build an image. Each instruction (FROM, COPY, RUN, CMD) runs in order and most of them add a layer.
It makes your build reproducible and reviewable: the environment lives in version control instead of a wiki page of setup steps.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]A virtual machine boots a full guest operating system on top of a hypervisor, so each VM carries its own kernel and takes gigabytes and tens of seconds to start. A container shares the host kernel and isolates processes with namespaces and cgroups, so it's megabytes and starts in milliseconds.
The trade-off: VMs give stronger isolation and can run a different kernel, while containers give speed and density. Many real setups run containers inside VMs to get both.
| Container | Virtual machine | |
|---|---|---|
| Kernel | Shares host kernel | Own guest kernel |
| Size | Megabytes | Gigabytes |
| Start time | Milliseconds | Tens of seconds |
| Isolation | Process-level | Hardware-level, stronger |
Key point: Don't claim containers replace VMs. Saying 'containers inside VMs is common' shows you know how real infrastructure is built.
Watch a deeper explanation
Video: Docker Crash Course for Absolute Beginners [NEW] (TechWorld with Nana, YouTube)
A registry is a service that stores and distributes Docker images. Docker Hub is the default public registry, but private registries like Amazon ECR, GitHub Container Registry, or a self-hosted one are common in companies.
You push images to a registry and pull them on other machines. An image reference looks like registry/repository:tag, and when the registry is omitted Docker assumes Docker Hub.
docker pull nginx:1.27 # pull from Docker Hub
docker tag myapp:1.0 registry.example.com/team/myapp:1.0
docker push registry.example.com/team/myapp:1.0The daily set is small: build an image, run a container, list what's running, view logs, exec into a shell, stop, and remove. Knowing these cold is the baseline any interviewer expects.
The pattern to remember: docker <object> <action>, so docker container ls and docker image ls read cleanly, though the older short forms like docker ps still work.
docker run -d --name web -p 8080:80 nginx
docker ps
docker logs web
docker exec -it web sh
docker stop web && docker rm web-d runs the container detached in the background instead of tying up your terminal. -p publishes a container port to the host as host:container. -it combines interactive (-i) and a TTY (-t) so you can use a shell inside the container.
You'd use -d -p for a service you want running in the background, and -it for debugging or a one-off shell session.
docker run -d -p 8080:80 nginx # background service on host port 8080
docker run -it ubuntu bash # interactive shell in a fresh containerENTRYPOINT sets the executable that always runs; CMD sets default arguments or a default command that's easy to override at runtime. If both exist, CMD's values are passed as arguments to ENTRYPOINT.
Use ENTRYPOINT when the container is really one program (like a CLI tool) and CMD for defaults you expect users to swap. Anything after the image name on docker run replaces CMD.
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
# docker run img -> python app.py --port 8080
# docker run img --port 9090 -> python app.py --port 9090Key point: The trap is saying they're interchangeable. The CMD-as-arguments-to-ENTRYPOINT relationship is exactly what this question screens for.
RUN executes at build time to create a new image layer, like installing packages or compiling code. CMD does not run during the build at all; it sets the default command that executes when a container starts from the finished image. So RUN shapes the image, CMD shapes the runtime.
A Dockerfile can have many RUN instructions but only one effective CMD. Mixing them up is a classic beginner mistake that leads to 'why isn't my install happening' confusion.
COPY copies files and directories from the build context into the image, plainly and predictably. ADD does the same but adds two behaviors: it auto-extracts local tar archives into the destination and can fetch remote URLs. That extra magic is exactly why COPY is usually the safer default.
Best practice is to prefer COPY for clarity and use ADD only when you specifically want tar auto-extraction. Fetching remote URLs with ADD is discouraged because it's harder to cache and audit.
A container has its own network namespace, so a service listening on port 80 inside the container isn't reachable from the host until you map it. -p host:container tells Docker to forward traffic from the host port to the container port.
So -p 8080:80 means requests to the host's port 8080 reach port 80 inside the container. Without a mapping, the port is only reachable from other containers on the same network.
docker run -d -p 8080:80 nginx # host 8080 -> container 80
curl http://localhost:8080 # reaches nginxA volume is Docker-managed storage that lives outside a container's writable layer, so the data survives when the container is removed. Without a volume, anything a database writes is gone the moment the container is deleted.
Volumes are the recommended way to persist data because Docker manages their location and lifecycle. You attach one with -v or --mount and multiple containers can share it.
docker volume create pgdata
docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:16
# remove and recreate db; the data in pgdata persistsA volume is stored in a location Docker manages and is the preferred way to persist container data. A bind mount maps a specific host directory straight into the container, so you control the exact path on the host.
Volumes are portable and easier to back up; bind mounts are handy in development for live-editing source code from your machine into the container.
| Volume | Bind mount | |
|---|---|---|
| Managed by | Docker | You (host path) |
| Location | Docker's storage area | Any host directory |
| Best for | Production data, databases | Local dev, live code editing |
| Portability | Higher | Tied to host layout |
A container moves through created, running, paused, stopped (exited), and removed. docker create makes it without starting, docker start runs it, docker stop sends SIGTERM then SIGKILL, and docker rm deletes a stopped container.
Understanding this explains why a container can exit immediately: if its main process finishes, the container stops, because a container lives only as long as its process 1.
docker create --name c1 nginx # created
docker start c1 # running
docker stop c1 # exited
docker rm c1 # removedA container runs only as long as its main process (PID 1) keeps running. If that process finishes, exits with an error, or isn't a long-running foreground process, the container stops the instant the process does. A container is not a machine that stays up on its own; it's a wrapper around one process.
Common causes: the CMD is a one-shot command, the app daemonizes itself into the background so PID 1 exits, or the entrypoint crashes. Checking docker logs almost always reveals it.
Key point: This tests whether you understand that a container isn't a VM. The PID 1 insight is what separates a real answer from a guess.
docker ps lists running containers and docker ps -a includes stopped ones. docker images (or docker image ls) lists local images. For details, docker inspect returns full JSON metadata about a container or image.
docker logs shows a container's output and docker stats streams live CPU and memory usage, which is the fastest way to spot a runaway container.
docker ps -a
docker images
docker inspect web | grep IPAddress
docker statsA tag is a human-readable label on an image, written as repository:tag, like nginx:1.27. It lets you version images and pull a specific build rather than whatever happens to be current. Without an explicit tag, Docker fills in a default, and that default is where people get surprised.
The catch: latest is not special, it's just the default tag Docker uses when you omit one. It does not always mean the newest version, so pinning explicit tags in production avoids surprise upgrades.
Key point: Saying 'latest is just a default tag, not the newest build' is the exact misconception this question is checking for.
An image is built from stacked read-only layers, one per Dockerfile instruction that changes the filesystem. Layers are cached and shared: if two images use the same base, that base layer is stored once on disk.
When you run a container, Docker adds a thin writable layer on top. This layering is why images are efficient to store and fast to pull, only the changed layers move.
docker stop sends SIGTERM to let the process shut down gracefully, then SIGKILL after a grace period (10 seconds by default). docker kill sends SIGKILL immediately with no chance to clean up.
Prefer stop so the app can flush buffers and close connections. Reach for kill only when a container is hung and ignoring the polite request.
In the foreground (the default), the container's output streams to your terminal and it stops when you press Ctrl+C. Detached (-d) runs it in the background and returns the container ID immediately.
Use foreground for quick tests where you want to watch output, and detached for services you want to keep running while you do other work.
For candidates with working experience: image optimization, networking, Compose, and the questions that separate users from understanders.
Docker builds images layer by layer and caches each one. On a rebuild, it reuses a cached layer as long as that instruction and everything it depends on are unchanged. Once one layer misses the cache, every layer after it rebuilds too.
So order instructions from least to most frequently changing: copy dependency manifests and install dependencies before copying your source code. That way a code edit doesn't force a full dependency reinstall.
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci # cached unless package files change
COPY . . # only this layer rebuilds on a code edit
CMD ["node", "server.js"]Key point: The killer follow-up is 'why put COPY package.json before COPY .?'. The cache-invalidation-cascades-downward answer is the whole point.
Watch a deeper explanation
Video: Docker Tutorial for Beginners [FULL COURSE in 3 Hours] (TechWorld with Nana, YouTube)
A multi-stage build uses more than one FROM in a single Dockerfile. You build or compile in one stage that has all the tooling, then copy only the finished artifacts into a clean, minimal final stage.
The payoff is a much smaller and safer final image: compilers, build dependencies, and source code stay behind in the discarded build stage. A Go binary can ship in a scratch image of a few megabytes.
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN go build -o /app ./cmd/server
FROM gcr.io/distroless/base
COPY --from=build /app /app
ENTRYPOINT ["/app"]Key point: Being able to write the COPY --from=build line from memory is the bar. The follow-up is usually 'how much smaller is the image?'.
Watch a deeper explanation
Video: Docker Tutorial for Beginners: A Full DevOps Course on Running Apps in Containers (freeCodeCamp.org, YouTube)
Start from a slim or distroless base, use multi-stage builds so build tools don't ship, combine related RUN steps and clean package caches in the same layer, and add a .dockerignore so junk never enters the build context.
The single biggest lever is usually the base image plus multi-stage. Switching from a full OS base to a slim variant and dropping build tooling often cuts an image by hundreds of megabytes.
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*A .dockerignore file lists paths Docker should exclude from the build context, which is the set of files sent to the daemon before a build starts. It works like .gitignore: one pattern per line, and anything matched never enters the context and can't be copied into the image.
It matters for speed and size: keeping .git, node_modules, and secrets out of the context makes builds faster, avoids cache busting from irrelevant file changes, and prevents accidentally copying secrets into an image.
.git
node_modules
*.log
.env
Dockerfile
.dockerignoreThe common ones are bridge (the default for standalone containers on one host), host (the container shares the host's network stack with no isolation), none (no networking), and overlay (connects containers across multiple hosts in a swarm).
For most single-host setups you use a user-defined bridge network so containers can reach each other by name. Overlay comes in when you need cross-host communication.
| Driver | Scope | Use it for |
|---|---|---|
| bridge | Single host | Default isolated container networking |
| host | Single host | Max performance, no port mapping |
| none | Single host | Fully disabling networking |
| overlay | Multi-host | Cross-host communication in swarm |
Put them on the same user-defined bridge network and they reach each other by container name, because Docker runs an embedded DNS server on user-defined networks. So a web container can hit http://db:5432 without knowing an IP.
The default bridge network does not give you name resolution, only IPs, which is why creating a named network (or using Compose, which does it for you) is the standard practice.
docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run -d --name web --network appnet myapp
# inside web, 'db' resolves to the database containerKey point: Naming the embedded DNS on user-defined networks (and that the default bridge lacks it) is the detail that indicates real hands-on experience.
Docker Compose defines a multi-container application in one YAML file: the services, their images, ports, volumes, environment variables, and the networks that connect them. One command, docker compose up, reads that file and starts the whole stack with everything wired together, and docker compose down tears it back down.
Use it when your app is more than one container, a web service plus a database plus a cache, and you want reproducible local environments and simple orchestration without a full cluster.
services:
web:
build: .
ports:
- "8080:80"
depends_on:
- db
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:No. depends_on controls start order, so the database container starts before the web container, but it does not wait for the database to be accepting connections. The web app can still start before Postgres is ready.
To wait for readiness, add a healthcheck and use depends_on with condition: service_healthy, or make the app retry its connection on startup. Retrying in the app is the more portable habit.
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 5
web:
depends_on:
db:
condition: service_healthyKey point: The 'depends_on only orders startup, not readiness' distinction is a favorite gotcha. Mentioning healthchecks or app-level retries closes it.
Environment variables are the standard way: set them with -e on docker run, an env_file, or the environment block in Compose. The app reads them at startup, which keeps config out of the image.
For anything sensitive, prefer secrets over plain env vars, since env vars show up in docker inspect and process listings. Never bake secrets into the image with ENV.
docker run -e LOG_LEVEL=debug -e PORT=8080 myapp
docker run --env-file .env myappKeep secrets out of the image entirely. Don't put them in ENV or COPY a .env into the image, because anyone who pulls the image or runs docker history can read them.
Inject them at runtime instead: Docker or Compose secrets mount them as files, or use your orchestrator's or cloud provider's secret manager. BuildKit secrets let you use a credential during a build without baking it into a layer.
# BuildKit secret: available during build, never stored in a layer
docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .HEALTHCHECK tells Docker how to test whether a container is actually working, not just whether its process is running. Docker runs the command you give it on an interval and marks the container healthy, unhealthy, or still starting based on the exit code. That status is visible in docker ps.
This matters because a process can be up but broken. Orchestrators and Compose use the health status to decide whether to route traffic, restart, or wait on dependents.
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1Restart policies tell Docker what to do when a container exits: no (never restart), on-failure (restart only on a non-zero exit, optionally capped), always (restart regardless, including on daemon start), and unless-stopped (like always, but respects a manual stop).
Use on-failure for jobs that should retry a few times and unless-stopped for long-running services you want back after a reboot without overriding a deliberate stop.
docker run -d --restart on-failure:3 worker
docker run -d --restart unless-stopped webdocker exec -it <container> sh (or bash) drops you into a shell inside the running container so you can inspect files, processes, and environment. docker logs shows its output, and docker inspect gives full config and network details.
For a container that won't stay up, check docker logs first, then override the entrypoint with docker run --entrypoint sh to poke around a fresh one. docker stats and docker top help with resource and process questions.
docker exec -it web sh
docker logs --tail 100 -f web
docker inspect web
docker run --entrypoint sh -it myapp # debug a crashing imageARG defines a build-time variable available only while the image is being built; it's gone by the time a container runs. ENV sets an environment variable that persists inside the running container and is readable by the app. So ARG configures the build, ENV configures the runtime, and they don't cross over unless you make them.
Use ARG for things like a version to install or a build flag, and ENV for configuration the app reads at runtime. A common pattern passes an ARG into an ENV when you want a build input to also be available at run time.
ARG APP_VERSION=1.0.0
ENV APP_VERSION=${APP_VERSION}
# build override: docker build --build-arg APP_VERSION=2.1.0 .Over time stopped containers, dangling images, unused networks, and orphaned volumes pile up and eat disk. docker system prune cleans several of these at once: it removes stopped containers, unused networks, and dangling images in a single command, and it prompts before it deletes anything.
Add -a to also remove images not used by any container, and --volumes to include unused volumes. Be deliberate with volumes, since that command can delete data you meant to keep.
docker system df # see what's using space
docker system prune # safe-ish cleanup
docker system prune -a --volumes # aggressive: removes unused images and volumesKey point: The senior touch is warning that --volumes can delete real data. Reckless prune advice is a small red flag interviewers notice.
EXPOSE in a Dockerfile is documentation: it records which port the container listens on but does not open anything to the host. Publishing with -p (or ports in Compose) actually maps a container port to a host port so outside traffic can reach it.
So a container with EXPOSE 80 still isn't reachable from your browser until you run it with -p 8080:80. EXPOSE is a hint for humans and tooling, not a networking action.
Key point: Candidates often think EXPOSE opens the port. Correcting that cleanly is exactly what the question is designed to surface.
COPY --from lets you pull files out of a previous build stage or even another image. It's the mechanism behind multi-stage builds and a handy way to grab a binary from an official image.
You reference a named stage (COPY --from=build) or an external image (COPY --from=busybox:latest /bin/busybox), keeping your final image lean.
FROM node:20 AS build
# ... build steps ...
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/htmlBy default a container's stdout and stderr are captured by a logging driver, json-file out of the box, and you read them with docker logs. The recommended app behavior is to log to stdout and stderr rather than to files inside the container.
In production you swap the logging driver (json-file, local, syslog, awslogs, or a fluentd/gelf pipeline) to ship logs to a central system, and you cap json-file size so logs don't fill the host disk.
docker run -d \
--log-driver json-file \
--log-opt max-size=10m --log-opt max-file=3 \
myappBind mount your source directory into the container so edits on the host appear instantly inside it, then run the app with a watcher that restarts on change. Compose makes this repeatable.
The step sequence is the same each time, and getting it wrong (mounting over installed dependencies, forgetting the watcher) is the usual source of 'my changes aren't showing up' frustration.
web:
build: .
volumes:
- ./src:/app/src
- /app/node_modules
command: npm run devLocal dev with live reload
Bind mounts shine in development; switch to a self-contained image with no host mounts for production.
The build context is the set of files Docker sends to the daemon when you run docker build. The path you pass (usually the trailing dot) is the root of that context, and COPY and ADD can only reach files inside it.
A bloated context, a directory with node_modules, large binaries, or the .git folder, makes every build slower because all of it gets packaged and sent first. A .dockerignore trims it.
advanced rounds probe internals, security, and production scars. Expect every answer here to draw a follow-up.
Two Linux primitives do the heavy lifting. Namespaces isolate what a process can see, its own view of PIDs, network interfaces, mounts, users, and hostnames, so a container thinks it has the machine to itself. Control groups (cgroups) limit and account for what it can use: CPU, memory, and I/O.
Docker orchestrates these plus a union filesystem (overlay2) for layered images and capabilities and seccomp for restricting syscalls. So a container isn't a special object, it's a normal process the kernel has been told to isolate and constrain.
Key point: Naming namespaces (isolation) and cgroups (limits) as the two pillars is the answer that indicates genuine understanding, not surface familiarity.
Watch a deeper explanation
Video: Complete Docker Course: From Beginner to Pro (Learn Containers) (DevOps Directive, YouTube)
Run as a non-root USER, start from a minimal or distroless base, pin exact image tags or digests, drop Linux capabilities you don't need, set a read-only root filesystem where possible, and scan images for known vulnerabilities in CI.
At runtime, avoid --privileged, don't mount the Docker socket into containers, set resource limits, and keep secrets out of images. Each of these shrinks the blast radius if something is compromised.
RUN adduser --system --no-create-home appuser
USER appuser
# docker run --read-only --cap-drop ALL --security-opt no-new-privileges myappKey point: This is a checklist question. Interviewers count how many independent controls you name; non-root plus minimal base plus no privileged is the core three.
The Docker daemon typically runs as root, and the socket (/var/run/docker.sock) is the full API to it. Any container with that socket mounted can create new containers, bind-mount the host filesystem, and effectively gain root on the host.
So mounting the socket is close to handing out host root. If a tool genuinely needs the API, prefer a rootless daemon, a socket proxy that filters requests, or a sidecar with tightly scoped access instead.
Key point: This is a security-awareness screen. Explaining that socket access equals host root is the answer; hand-waving 'it's a bit risky' misses the point.
overlay2 stacks the image's read-only layers as lower directories and gives the container a thin writable upper directory. Reads fall through the stack to the first layer that has the file; writes go to the upper layer.
The key mechanic is copy-on-write: modifying a file from a lower layer copies it up first, then edits the copy. That's why heavy in-container writes are slow and belong in a volume, and why the base layers stay shared and untouched across containers.
Because writes trigger a copy of the file into the writable layer, workloads that rewrite large files or do heavy random I/O inside the container's filesystem pay a copy-on-write penalty and bloat the writable layer.
The fix is to put write-heavy paths, databases, logs, caches, on a volume, which bypasses the union filesystem and writes directly. This is a common answer to 'my database is slow in a container'.
Plain Docker on a single host is fine for small services, but production usually needs scheduling across machines, self-healing, rolling updates, and service discovery. That's the job of an orchestrator, almost always Kubernetes today, with Docker Swarm as a lighter option.
The clean way to frame it: Docker (or a compatible runtime) builds and runs individual containers, while the orchestrator decides where they run, restarts failures, scales replicas, and routes traffic. Kubernetes actually drives a runtime like containerd underneath.
| Concern | Docker alone | Orchestrator (Kubernetes) |
|---|---|---|
| Scope | Containers on one host | Containers across a cluster |
| Self-healing | Restart policies only | Reschedules failed pods |
| Scaling | Manual | Declarative replicas, autoscaling |
| Rolling updates | Manual | Built in with rollback |
BuildKit is the modern build backend, default in current Docker. It builds independent stages in parallel, skips stages whose outputs aren't needed, caches more precisely, and supports build secrets and SSH forwarding that never end up in a layer.
Concretely it gives faster builds, better cache import/export for CI (so a fresh runner reuses a previous build's cache), and safer handling of credentials during builds. RUN --mount=type=cache also keeps package caches across builds.
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txtScan images for known CVEs in CI (Trivy, Grype, Docker Scout) and fail the build on high-severity findings, pin base images by digest, and rebuild regularly so base fixes land. Minimizing the image (distroless) also minimizes what can be vulnerable.
Going further: generate an SBOM so you know what's inside, sign images and verify signatures (cosign) so only trusted images deploy, and keep a private registry with access controls rather than pulling arbitrary public images.
The container's main process runs as PID 1, which the kernel treats specially: it ignores signals it has no handler for. If your app is PID 1 and doesn't handle SIGTERM, docker stop can't shut it down gracefully and waits out the timeout before a SIGKILL.
Also, if PID 1 spawns children (like a shell script starting your app), it may not reap zombies. The fixes: use exec form in CMD/ENTRYPOINT so your app is really PID 1, handle SIGTERM in the app, or add a minimal init like tini (docker run --init).
# exec form so the binary is PID 1 and receives signals
ENTRYPOINT ["node", "server.js"]
# vs shell form, which wraps it in /bin/sh and can swallow SIGTERMKey point: The exec-form-versus-shell-form detail plus tini is what separates people who've debugged slow shutdowns from those who've only read tutorials.
The container must catch SIGTERM (what docker stop and orchestrators send first) and use the grace period to stop taking new work, finish in-flight requests, close connections, and flush buffers before exiting. Only then does the SIGKILL fallback not matter.
In practice: trap SIGTERM in the app, keep the shutdown under the stop-grace period, and for Kubernetes drain from the load balancer via readiness plus a preStop hook. Skipping this causes dropped requests on every deploy.
Use docker buildx with a builder that targets multiple platforms, and push a multi-arch manifest. The registry then serves the right architecture automatically when someone pulls, an Apple Silicon laptop gets arm64, an x86 server gets amd64, from one tag.
This became routine once ARM servers and ARM laptops got common. The catch to mention is that cross-building can be slow via emulation, so CI often uses native builders per architecture and merges the manifest.
docker buildx build --platform linux/amd64,linux/arm64 \
-t registry.example.com/team/myapp:1.0 --push .Docker exposes cgroup limits through flags: --memory caps RAM (the kernel OOM-kills the container if it exceeds it) and --cpus or --cpu-shares bounds CPU. Without limits a single container can starve everything else on the host.
For the JVM and some runtimes, also make sure the app is container-aware so it sizes heaps to the cgroup limit, not the host's total memory, a classic 'why did my container get OOM-killed' cause.
docker run -d --memory 512m --cpus 1.5 myapp
# exceeding --memory triggers an OOM kill of the containerConfirm the cause first: docker inspect shows OOMKilled: true and exit code 137. Then decide whether the limit is too low or the app is leaking. docker stats and the app's own memory metrics over time tell them apart.
Fixes split by cause: raise --memory if the workload legitimately needs it, fix a leak or unbounded cache if usage climbs forever, and make managed runtimes read the cgroup limit so they don't assume the whole host's RAM.
docker inspect web --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
# true 137 -> the container was killed for exceeding its memory limitKey point: Exit code 137 and OOMKilled: true are the tells. Naming them shows you've actually debugged this, not just read about limits.
Rootless Docker runs the daemon and containers as an unprivileged user, using user namespaces to map the container's root to a normal host user. If a container is compromised, the attacker holds an ordinary user's privileges, not host root.
The trade-offs to name: some features get harder (certain networking, ports below 1024, some storage drivers), and performance can differ. It's a strong default for multi-tenant or untrusted workloads, which is also why Podman is daemonless and rootless by design.
There's a tension: fewer layers (chaining RUN steps) shrinks images but can bust the cache on any change, while many small layers cache well but can bloat. The modern answer sidesteps it with multi-stage builds plus BuildKit cache mounts.
Cache mounts (RUN --mount=type=cache) keep package-manager caches across builds without shipping them in the image, and multi-stage keeps build junk out of the final layers. So you get fast rebuilds and a small image at once, rather than trading one for the other.
They're layers of a stack. runc is the low-level tool that actually creates a container from an OCI spec by calling the kernel. containerd is the daemon that manages images, storage, and the lifecycle of containers, calling runc to run them. Docker is the developer-facing platform (CLI, build, networking) that sits on top of containerd.
This matters because Kubernetes talks to containerd (or CRI-O) directly through the CRI, not to Docker. The old Docker shim was removed, but the images you build with Docker still run fine because they follow the OCI standard.
| Component | Level | Responsibility |
|---|---|---|
| Docker | High | Build, CLI, networking, developer UX |
| containerd | Mid | Image and container lifecycle, storage |
| runc | Low | Creates the container via kernel calls |
The pattern is rolling replacement behind a load balancer with health checks: bring up new containers, wait until they pass readiness, shift traffic to them, then drain and stop the old ones. Never stop the old version before the new one is proven healthy.
On a single host you can script this with a reverse proxy; with an orchestrator it's built in as a rolling update with configurable surge and unavailability, plus automatic rollback if the new version fails its checks. Graceful shutdown on the old containers keeps in-flight requests from dropping.
Rolling zero-downtime deploy
Rollback is just the same flow in reverse, which is why health checks gating each step matter so much.
Immutable infrastructure means you never patch a running container in place. To change anything, you build a new image, test it, and replace the container. The running artifact is disposable and reproducible from the image plus its config.
The benefit is predictability: every environment runs the exact bytes you tested, config drift disappears, and rollback is just redeploying the previous image. It also implies containers should be stateless, with all real state pushed to volumes or external stores.
Keep the state outside the container's lifecycle: mount a volume for the data directory so the database survives container replacement, and treat the container as replaceable while the volume is the durable part. Back up the volume, not the container.
The honest senior take: containers are a natural fit for stateless services, and stateful ones need care around storage, backups, and failover. In a cluster you use persistent volumes and StatefulSets or a managed database rather than a bare container for anything critical.
Secrets leak when they're set with ENV, copied in via COPY, or passed as a plain --build-arg, because all of that persists in image layers. docker history and unpacking the layers reveal them, even if a later instruction deletes the file, since the earlier layer still contains it.
Prevent it with BuildKit secret mounts (available during build, never stored), multi-stage builds that leave the secret in a discarded stage, runtime injection instead of build-time, and scanning images for embedded credentials in CI. Deleting a secret in a later layer does not remove it from history.
docker history --no-trunc myapp:1.0 # inspect every layer's command
# a secret set via ENV or ARG shows up here, even if 'removed' laterKey point: The 'deleting it in a later layer doesn't erase it from history' point is the insight this question is really after.
Docker wins when you want one packaging format and a friendly command-line for building and running containers across dev, CI, and production. It trades the strong isolation of a full virtual machine for speed and density, since containers share the host kernel. Where teams reach past Docker: Podman for a daemonless, rootless-by-default engine, and containerd or CRI-O as the lower-level runtime that Kubernetes actually drives. Knowing these boundaries out loud is an interview signal: it shows you pick tools on merits.
| Technology | Isolation level | Starts in | Best at |
|---|---|---|---|
| Docker container | Shared kernel, namespaces + cgroups | Milliseconds | App packaging, dev-to-prod parity |
| Virtual machine | Full guest OS + hypervisor | Tens of seconds | Strong isolation, different kernels |
| Podman | Same as Docker, no daemon | Milliseconds | Rootless, daemonless workflows |
| containerd | Low-level runtime under Docker/K8s | Milliseconds | Being driven by orchestrators |
Prepare in layers, and practice out loud. Most Docker rounds move from concept questions to writing or fixing a Dockerfile to a design or debugging discussion, so rehearse each stage rather than only reading answers.
The typical Docker interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Docker questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works