Docker interview questions & answers
Containers explained: images and layers, writing efficient Dockerfiles, multi-stage builds, volumes, networking, Compose and security.
Top 57 Docker interview questions most asked first
1.What is Docker, and what problem does it solve?easy
Docker is a platform for building, shipping and running applications in containers. You package the app together with its runtime, libraries and config into an image, and that image runs the same way on a laptop, a CI runner or a production server.
The main problem it solves is environment drift: "works on my machine" failures caused by different OS packages, language versions or config. It also gives you:
- Isolation: each container gets its own filesystem, process tree and network stack, without the cost of a full VM
- Fast startup: a container is just an isolated process, so it starts about as fast as the app itself
- Repeatable builds: a Dockerfile describes the image as code, and layers are cached and shared
- A standard unit of deployment that Compose, Kubernetes, ECS and CI systems all understand
Under the hood, Docker is a client (the
dockerCLI), a daemon (dockerd) and lower-level runtimes (containerd and runc) that use Linux namespaces and cgroups.What interviewers listen for- Packages the app and its dependencies into a portable image
- Same image runs identically in every environment
- Isolated processes, much lighter than VMs
- Dockerfile makes builds repeatable and versioned
- CLI, daemon, containerd and runc under the hood
Likely follow-up: What is the difference between Docker Engine and Docker Desktop?
2.How is a container different from a virtual machine?easy
A virtual machine virtualises hardware. A hypervisor runs full guest operating systems, each with its own kernel, so a VM carries a whole OS, takes longer to boot and needs more memory.
A container virtualises the operating system. All containers on a host share the host's kernel. Each one is just a process, or a group of processes, isolated with Linux namespaces (what it can see) and limited with cgroups (what it can use). There's no guest kernel, so containers start quickly, have little overhead, and you can pack far more of them onto a host.
The trade-offs:
- Isolation: the VM boundary is stronger. A kernel exploit inside a container can affect the host and every other container on it
- OS flexibility: a Linux container needs a Linux kernel. On macOS and Windows, Docker Desktop runs containers inside a lightweight Linux VM
- They're complementary: in the cloud, containers usually run inside VMs
What interviewers listen for- VMs virtualise hardware, each with its own kernel
- Containers share the host kernel
- Namespaces isolate, cgroups limit resources
- Containers are lighter and start faster
- VMs give a stronger isolation boundary
Likely follow-up: How does Docker Desktop run Linux containers on a Mac? · What problem do gVisor or Kata Containers solve?
3.What is the difference between a Docker image and a container?easy
An image is a read-only template: a stack of filesystem layers plus metadata such as the default command, environment variables, exposed ports and working directory. It's identified by a content digest and usually referenced by a tag like
nginx:alpine.A container is a running (or stopped) instance of an image. When you create one, Docker adds a thin writable layer on top of the image's read-only layers and gives the process its own namespaces, network interface and configuration.
The usual analogy is class versus object: one image, many containers. Each container's changes go into its own writable layer, so they don't affect the image or other containers, and they're gone when the container is removed. That's why data you need to keep belongs in a volume, and why you change an image by rebuilding it, not by editing a running container.
What interviewers listen for- Image: read-only layers plus config metadata
- Container: a runnable instance with a writable layer
- Many containers can share one image
- Container changes are lost when it is removed
- Persist data in volumes; change images by rebuilding
Likely follow-up: What does
docker commitdo, and why is it discouraged?4.Walk me through a simple Dockerfile for a Node.js API. What does each instruction do?easy
A Dockerfile is a recipe the builder runs top to bottom. Most instructions either add a layer or change the image's config:
FROMpicks the base image, here Node 22 on slim DebianWORKDIRsets, and creates if needed, the directory later instructions and the container use- The first
COPYbrings in only the dependency manifests, so thenpm cilayer stays cached until dependencies change RUNexecutes a command at build time and saves the result as a layer- The second
COPYadds the source code ENVsets an environment variable for later steps and for the running containerEXPOSEdocuments the port the app listens on; it doesn't publish itUSERswitches to the non-rootnodeuser the official image providesCMDis the default command at run time, in exec form sonoderuns as PID 1 and receives signals
Build it with
docker build -t my-api .and run it withdocker run -p 3000:3000 my-api.FROM node:22-bookworm-slim WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY . . ENV NODE_ENV=production EXPOSE 3000 USER node CMD ["node", "server.js"]What interviewers listen for- FROM sets the base, WORKDIR the working directory
- RUN executes at build time, CMD at run time
- Copy dependency manifests before source for caching
- EXPOSE documents;
-pactually publishes - Run as non-root with USER
Likely follow-up: Why use
npm cirather thannpm installin a Dockerfile?5.What is the difference between
CMDandENTRYPOINT? What doesdocker run my-image thereprint for this Dockerfile?midENTRYPOINTdefines the executable the container always runs;CMDsupplies default arguments, or the default command when there's no entrypoint. In exec form, Docker runsENTRYPOINTfollowed byCMD.docker run my-imageprintshello worlddocker run my-image thereprintshello there: arguments after the image name replaceCMD, not the entrypointdocker run --entrypoint date my-imagereplaces the entrypoint and also clears the image's defaultCMD
Shell form is different:
ENTRYPOINT echo hellois wrapped in/bin/sh -cand ignores bothCMDand any run arguments, so it would print justhello.A common pattern is
ENTRYPOINT ["/docker-entrypoint.sh"]withCMD ["postgres"]: the script does setup, then runsexec "$@"so the real process replaces the shell. UseCMDalone when users should freely swap the command, andENTRYPOINTwhen the image behaves like a tool.FROM alpine:3.20 ENTRYPOINT ["echo", "hello"] CMD ["world"]What interviewers listen for- ENTRYPOINT is the executable, CMD the default arguments
- Exec form runs ENTRYPOINT followed by CMD
docker runarguments replace CMD only--entrypointoverrides the entrypoint and clears CMD- Shell-form ENTRYPOINT ignores CMD and run arguments
Likely follow-up: Why do entrypoint scripts usually end with
exec "$@"?6.What is the difference between
COPYandADDin a Dockerfile, and which should you use?easyBoth copy files into the image, but
ADDhas extra behaviours:- A local tar archive (gzip, bzip2, xz and similar) is automatically extracted into the destination
- The source can be a remote URL, which is downloaded. A remote tar file is not extracted by default; recent Dockerfile syntax adds
--unpackto control this - Current Dockerfile versions also accept a Git repository URL, and
--checksumto verify a remote source
COPYsimply copies files from the build context, or with--from, from another build stage or image. Docker's guidance is to preferCOPYbecause it's explicit and predictable; the auto-extraction inADDcan surprise whoever reads the Dockerfile later. Reach forADDonly when you actually want one of its features, typically unpacking a local tarball.Both support
--chownand--chmod, which set ownership and permissions as the files are copied. That's better than a laterRUN chown -R, which would duplicate every file into a new layer.What interviewers listen for- COPY copies from the context or another stage
- ADD auto-extracts local tar archives
- ADD can also fetch URLs and Git repos
- Remote tarballs are not auto-extracted
- Prefer COPY unless you need an ADD feature
Likely follow-up: Why does
RUN chown -Rafter aCOPYincrease image size?7.What are image layers, and how does the build cache decide whether to reuse a layer?mid
An image is an ordered stack of read-only layers. Instructions that change the filesystem, mainly
RUN,COPYandADD, each produce a layer holding just the files they added, changed or deleted. Others, likeENVorCMD, only change metadata. Layers are content-addressed, so identical layers are stored once and shared between images, and only missing layers are pulled or pushed.When building, the builder walks the instructions in order and looks for a cached result:
- For most instructions, a match means the same instruction on top of the same parent
- For
COPYandADD, the checksums of the source files are part of the key, so editing a copied file invalidates it RUNis matched on its command string, not its output:RUN apt-get updatewon't re-run just because the upstream repository changed
The key rule: once one step misses the cache, every step after it is rebuilt. So you order instructions from least to most frequently changing, and use
--no-cacheor--pullwhen you need fresh results.What interviewers listen for- Filesystem-changing instructions create layers
- Layers are content-addressed and shared between images
- COPY/ADD cache keys include file checksums
- RUN is matched on its command string
- One cache miss rebuilds every later step
Likely follow-up: Why is
RUN apt-get updateon its own line an anti-pattern?8.What is a multi-stage build, and why would you use one?mid
A multi-stage build has several
FROMinstructions in one Dockerfile. Each starts a new stage, and youCOPY --from=<stage>only the artifacts you need into the final one. Compilers, dev dependencies, source code and build caches stay behind in earlier stages and never ship.Benefits:
- Much smaller images, because the runtime stage can use a slim or distroless base
- Smaller attack surface: no build tools in production
- One Dockerfile instead of separate build scripts
- With BuildKit, independent stages build in parallel, and stages the target doesn't depend on are skipped
docker build --target build .stops at a named stage, which is handy for running tests in CI from the same Dockerfile. For compiled languages the win is dramatic: build a Go binary with the full toolchain, then copy just that binary into ascratchor distroless image.FROM node:22-bookworm AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build && npm prune --omit=dev FROM node:22-bookworm-slim WORKDIR /app ENV NODE_ENV=production COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist USER node CMD ["node", "dist/server.js"]What interviewers listen for- Multiple FROM stages in one Dockerfile
COPY --fromtakes only the needed artifacts- Build tools and dev dependencies never ship
--targetbuilds up to a specific stage- BuildKit skips unused stages and runs others in parallel
Likely follow-up: How would you run unit tests as part of a multi-stage build?
9.Compare volumes, bind mounts and tmpfs mounts. When would you use each?mid
All three keep data out of the container's writable layer, but the data lives in different places:
- Volumes are managed by Docker (under
/var/lib/docker/volumeson Linux). They survive container removal, behave the same on every OS, can use drivers for remote storage, and an empty volume is pre-populated with whatever the image has at that path. They're the default choice for persistent data like databases. - Bind mounts map an exact host path into the container. Ideal for development (edit on the host, run in the container) or injecting config, but they depend on the host's layout and permissions, and they hide whatever the image had at that path.
- tmpfs mounts live only in memory and vanish when the container stops. Use them for scratch space or sensitive temporary files that should never hit disk.
One gotcha: with
-v, a missing host path is silently created as a directory, while--mount type=bindfails with an error.# Named volume: managed by Docker, survives container removal docker run -d -e POSTGRES_PASSWORD=dev \ -v pgdata:/var/lib/postgresql/data postgres:16-alpine # Bind mount: a host directory, e.g. live code in development docker run --rm -it --mount type=bind,src="$(pwd)",dst=/app -w /app node:22 npm test # tmpfs: memory only, discarded when the container stops docker run --rm --tmpfs /scratch:size=64m alpine:3.20 df -h /scratchWhat interviewers listen for- Volumes: Docker-managed, the default for persistent data
- Bind mounts: exact host path, great for development
- tmpfs: in memory, gone when the container stops
- Empty volumes are pre-populated from the image
- Mounts hide existing image content at that path
Likely follow-up: How would you back up a named volume?
- Volumes are managed by Docker (under
10.This Dockerfile reinstalls every dependency whenever any source file changes. Why, and how would you fix it?mid
COPY . .copies the whole build context, and its cache key includes the checksum of every file. Change one line of source and that step misses the cache, soRUN npm ciafter it, and every later step, runs again.The fix is to order instructions from least to most frequently changing and copy only what each step needs:
COPY package.json package-lock.json ./RUN npm ciCOPY . .
Now the install layer is only invalidated when the manifests change; a code edit rebuilds just the final
COPY. The same idea applies everywhere:requirements.txtbefore the Python source,go.modandgo.sumbeforego mod download.Two more things help: a
.dockerignoresonode_modules,.gitand logs don't invalidate the cache or bloat the context, and a BuildKit cache mount for the package manager's cache, so even a dependency change doesn't download everything from scratch.FROM node:22-bookworm-slim WORKDIR /app COPY . . RUN npm ci CMD ["node", "server.js"]What interviewers listen for- COPY cache key includes checksums of copied files
- A cache miss rebuilds all later steps
- Copy manifests, install, then copy source
- .dockerignore avoids needless invalidation
- Cache mounts speed up dependency reinstalls
Likely follow-up: How would you share the build cache between CI runs?
11.What is Docker Compose, and when would you use it?easy
Docker Compose lets you define a multi-container application in one YAML file, usually
compose.yaml, and manage it as a unit. Instead of a pile ofdocker runflags, you declare services (each becomes one or more containers) plus the networks and volumes they use.Key behaviours:
docker compose up -dbuilds or pulls images, creates a project network and starts everything;docker compose downremoves the containers and network, anddown -vdeletes the volumes too- Every service joins the project network, so
apireaches the database at the hostnamedb depends_oncontrols start order, and with a health-check condition it can wait for readiness- Values like
${TAG}are interpolated from the shell environment or a.envfile
It's ideal for local development, integration tests in CI and small single-host deployments. For multi-host scheduling, self-healing and rolling updates, you'd move to Kubernetes or a managed orchestrator.
services: api: build: . ports: ["3000:3000"] environment: DATABASE_URL: postgres://postgres:dev@db:5432/postgres depends_on: [db] db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: dev volumes: [pgdata:/var/lib/postgresql/data] volumes: pgdata:What interviewers listen for- Declares services, networks and volumes in YAML
up -dstarts the stack,downremoves it- Services reach each other by service name
depends_oncontrols start order- Single-host tool; Kubernetes for clusters
Likely follow-up: What is the difference between
docker composeand the olddocker-compose?12.Your image is 1.5 GB. How would you make it smaller?mid
I'd work through these, roughly in order of impact:
- Pick a smaller base: a
-slimDebian variant, Alpine or distroless instead of the full default tag - Use a multi-stage build so compilers, dev dependencies and source stay in the build stage
- Add a
.dockerignoreso.git,node_modules, test data and logs never get copied in - Clean up in the same
RUNthat made the mess, e.g.apt-get install --no-install-recommends ... && rm -rf /var/lib/apt/lists/*. Deleting files in a later instruction doesn't shrink the image, because the earlier layer still contains them - Install only production dependencies, and use BuildKit cache mounts rather than leaving package caches in layers
- Avoid
RUN chown -Rover large trees, which copies every file into a new layer; useCOPY --chowninstead
To find the culprit,
docker history <image>shows how much each instruction added, and tools likedivelet you browse layer contents. Smaller images pull and start faster and have fewer packages to patch.What interviewers listen for- Slim, Alpine or distroless base images
- Multi-stage builds leave build tools behind
- Clean up within the same RUN layer
- Deleting in a later layer does not shrink the image
docker historyshows size per instruction
Likely follow-up: Why does
RUN rm big-fileas a separate step not reduce the image size?- Pick a smaller base: a
13.What network drivers does Docker provide, and when would you use each?mid
The main drivers:
- bridge (default): a private virtual network on one host. Containers get their own IPs, reach the outside world through NAT, and are reachable from outside only through published ports. A user-defined bridge (
docker network create) also gives DNS between containers - host: the container shares the host's network stack. No isolation and no port mapping (
-pis ignored with a warning), but no NAT overhead. Mainly a Linux feature; Docker Desktop supports it only from 4.34 as an opt-in - none: just a loopback interface, for jobs that need no network at all
- overlay: spans multiple Docker hosts. It requires Swarm mode, and standalone containers can join only if the network was created with
--attachable - macvlan / ipvlan: give containers addresses directly on the physical network, for legacy apps that expect to be on the LAN
The distinction interviewers usually want is bridge versus host: bridge gives isolation and port mapping, host trades isolation for simplicity and raw network performance.
What interviewers listen for- bridge: default, single host, NAT and published ports
- host: shares host network stack,
-pignored - none: loopback only
- overlay: multi-host, requires Swarm mode
- macvlan/ipvlan: addresses on the physical LAN
Likely follow-up: Why might you run a high-throughput proxy with host networking?
- bridge (default): a private virtual network on one host. Containers get their own IPs, reach the outside world through NAT, and are reachable from outside only through published ports. A user-defined bridge (
14.Two containers on the same host need to talk to each other. How do you set that up, and why can't they reach each other by name on the default bridge?mid
Put both containers on the same user-defined network. For user-defined networks, Docker runs an embedded DNS server, visible as
127.0.0.11inside the container, so containers resolve each other by container name, by any--network-alias, and in Compose by service name.The default
bridgenetwork, which containers join when you don't pass--network, doesn't offer that DNS. Containers on it can only reach each other by IP address, unless you use the legacy--linkflag. Docker's docs treat the default bridge as a legacy detail and recommend user-defined networks, which also isolate stacks from each other instead of putting every container on one shared network.Two related points: containers talk to each other on the container port (
web:80), not on a published host port, and you don't need-pat all for container-to-container traffic. Publishing is only for reaching a container from outside Docker's networks.docker network create app-net docker run -d --name web --network app-net nginx:alpine docker run --rm --network app-net alpine:3.20 wget -qO- http://webWhat interviewers listen for- Use a user-defined network
- Embedded DNS resolves container and service names
- Default bridge has no name resolution
- Connect on the container port, not the published one
- No
-pneeded between containers
Likely follow-up: How can one container be attached to two networks at once?
15.What is the difference between
EXPOSEin a Dockerfile and-pondocker run?easyEXPOSE 80is documentation: it records in the image metadata which port the app listens on. It doesn't open or publish anything.Publishing happens at run time:
-p 8080:80maps host port 8080 to container port 80-p 127.0.0.1:8080:80binds only on the host's loopback, so other machines can't reach it-Ppublishes every exposed port to random high host ports, which is the one placeEXPOSEhas a real effect
By default,
-pbinds on all host interfaces, which the Docker docs call "insecure by default": the service is reachable from the network, and on Linux Docker's NAT rules divert traffic before firewalls likeufwsee it. For local-only services, such as a dev database, bind to127.0.0.1.Also make sure the app inside listens on
0.0.0.0, not127.0.0.1; otherwise the port is published but nothing answers.What interviewers listen for- EXPOSE is documentation only
-p host:containerpublishes a port-Ppublishes all exposed ports to random ports- Default binding is all interfaces
- The app must listen on 0.0.0.0 inside
Likely follow-up: Why does traffic to a published port bypass
ufwrules?16.What is the difference between
ARGandENVin a Dockerfile?midBoth define variables, but they live for different amounts of time:
ARGis a build-time variable. You set it withdocker build --build-arg APP_VERSION=1.2, later instructions in that stage can use it, and it's not in the running container's environmentENVis stored in the image config, so it's available to later build steps and to every container at run time, wheredocker run -ecan override it
Scope gotchas: an
ARGdeclared beforeFROMcan only be used inFROMlines; redeclare it inside the stage to use it there. Each stage of a multi-stage build needs its ownARGdeclarations.Neither is safe for secrets.
ENVvalues are baked into the image config, and build-arg values appear indocker history. Use a BuildKit secret mount for build-time secrets and inject runtime secrets when the container starts.The snippet shows the common trick of copying an
ARGinto anENVto keep a build-time value at run time.ARG NODE_VERSION=22 FROM node:${NODE_VERSION}-bookworm-slim ARG APP_VERSION=dev ENV NODE_ENV=production \ APP_VERSION=${APP_VERSION} RUN echo "building $APP_VERSION"What interviewers listen for- ARG is build-time only, set with
--build-arg - ENV persists into the image and containers
- ARG before FROM is only usable in FROM
- Build-arg values are visible in
docker history - Neither is appropriate for secrets
Likely follow-up: How can you pass a private npm token to a build without leaking it?
17.What is the build context, and why should you add a
.dockerignorefile?easyIn
docker build ., the.is the build context: the files the builder can read forCOPYandADD. A.dockerignorefile in the root of the context lists patterns to exclude, much like.gitignore.Why it matters:
- Speed: less data to send to the builder, which matters most with a remote builder
- Cache stability:
COPY . .isn't invalidated by changes to.git, logs or local build output - Correct images: your host's
node_modules, possibly compiled for another OS, doesn't overwrite the one installed in the image - Security:
.envfiles, keys and credentials don't end up in a layer by accident
A line starting with
!re-includes a file, and the last matching line wins. You can also give a specific Dockerfile its own ignore file, such asbuild.Dockerfile.dockerignorenext tobuild.Dockerfile, which takes precedence over the one at the root of the context.What interviewers listen for- Build context is the files the builder can access
- Excludes files like .gitignore does
- Faster builds and stable cache
- Keeps secrets and host artifacts out of images
!re-includes; the last matching rule wins
Likely follow-up: Does
.dockerignoreaffect files mounted as volumes at run time?18.A container exits immediately or keeps restarting. How do you debug it?mid
I work from the outside in:
docker ps -ashows the state and exit code, such asExited (1)orRestartingdocker logsshows what the process wrote to stdout and stderr, and it works on stopped containers. Most crashes are explained here: a missing env var, bad config, an unreachable databasedocker inspectgivesState.ExitCode,State.OOMKilled,State.Errorand the restart count, plus the exact command, env and mounts the container got- The exit code narrows it down: 137 means SIGKILL (often the OOM killer), 139 a segfault, 127 command not found, 126 not executable
- If it dies too fast to
execinto, override the entrypoint with a shell and run the start command by hand docker cpcopies files out of a stopped container, anddocker eventsshows the start, die and oom sequence
Typical root causes: the main process exits because it daemonised or had nothing to do, permission errors after switching to a non-root user, or
exec format errorfrom an image built for a different CPU architecture.docker ps -a --filter name=api # status and exit code docker logs --tail 100 api # output from the last run docker inspect api --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.RestartCount}}' docker events --filter container=api docker run -it --rm --entrypoint sh my-image # reproduce by hand docker cp api:/app/logs ./logs # works on stopped containersWhat interviewers listen fordocker ps -afor state and exit codedocker logsworks on stopped containersdocker inspectfor OOMKilled, error and config- Override the entrypoint to reproduce interactively
- Exit codes: 137 SIGKILL/OOM, 127 not found
Likely follow-up: How do you debug a distroless container that has no shell?
19.Describe the container lifecycle. How do
docker stop,docker killanddocker rmdiffer?easyA container moves through a few states: created (
docker create: config and writable layer exist, nothing running), running (docker start, ordocker run, which is create plus start), paused (docker pausefreezes its processes using the cgroup freezer), exited (the main process ended or was stopped) and finally removed.The commands differ in how they end it:
docker stopsends the stop signal (SIGTERMunless the image setsSTOPSIGNAL), waits a grace period, 10 seconds by default for Linux containers, then sendsSIGKILL. That gives the app a chance to shut down cleanlydocker killsendsSIGKILLstraight away by default, or any signal you choose with-sdocker rmdeletes a stopped container and its writable layer;-fkills and removes a running one
A stopped container still exists, keeps its writable layer and logs, and can be started again. Use
docker run --rmfor throwaway containers so they're removed when they exit.What interviewers listen for- Created, running, paused, exited, removed
docker runis create plus start- stop: SIGTERM, grace period, then SIGKILL
- kill: SIGKILL immediately by default
- Stopped containers keep their writable layer
Likely follow-up: How do you give a slow-shutdown app more time to stop?
20.What restart policies does Docker support, and how do
alwaysandunless-stoppeddiffer?easyA restart policy tells the Docker daemon what to do when a container's main process exits:
no(default): never restart automaticallyon-failure[:max-retries]: restart only after a non-zero exit code, optionally capped, e.g.on-failure:5always: restart whenever it stops, and start it again when the daemon starts. If you stop it manually, it stays stopped until the daemon restarts or you start it yourselfunless-stopped: likealways, except a container you stopped manually stays stopped even after a daemon restart
Set it with
docker run --restart unless-stopped, change it on an existing container withdocker update --restart, or userestart:in Compose. The daemon doubles the delay between attempts, starting at 100 ms, so a crash loop doesn't hammer the host.Restart policies are single-host crash recovery, not orchestration: they don't move containers to another machine, and a failing health check alone doesn't trigger a restart. Swarm and Kubernetes add that.
What interviewers listen for- no, on-failure, always, unless-stopped
- on-failure restarts only on a non-zero exit
- unless-stopped keeps manual stops across daemon restarts
- Increasing back-off delay between restarts
- Unhealthy status alone does not trigger a restart
Likely follow-up: How would you make a container restart when it becomes unhealthy?
21.In Compose, your API starts before Postgres is ready and crashes. Doesn't
depends_onfix that?midPlain
depends_on: [db]only controls start order. Compose starts thedbcontainer first, but it doesn't wait for Postgres to accept connections, so the API can start, fail to connect and exit.The long syntax adds a condition:
service_started: the default, same as the short formservice_healthy: wait until the dependency's health check passes. The dependency needs ahealthcheck, defined in Compose or in its imageservice_completed_successfully: wait for a one-off job, like a migration container, to exit with code 0
With
service_healthy,docker compose upshows the database as Waiting, then Healthy, and only then starts the API.Even so, the app should still retry connections with back-off.
depends_ononly matters at startup: if the database restarts later, or you deploy to Kubernetes where there's nodepends_on, resilient connection handling is what keeps the service up.services: api: build: . depends_on: db: condition: service_healthy db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: dev healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s retries: 10What interviewers listen for- Short form only orders container startup
service_healthywaits for the health checkservice_completed_successfullyfor one-off jobs- The dependency needs a healthcheck defined
- Apps should still retry connections
Likely follow-up: How would you run database migrations before the API starts?
22.What does the
HEALTHCHECKinstruction do, and what happens when a container becomes unhealthy?midHEALTHCHECKtells Docker how to test whether the app is actually working, not just whether its process is alive. Docker runs the command inside the container on a schedule: exit code0means healthy,1means unhealthy.The status starts as
starting, becomeshealthyafter a passing check, and turnsunhealthyafter--retriesconsecutive failures. The defaults are a 30s interval, 30s timeout, 0s start period and 3 retries. Failures during--start-perioddon't count, which gives slow apps time to boot.docker psshows the status anddocker inspectshows recent probe output.It's used by Compose's
condition: service_healthy, by Swarm to replace unhealthy tasks, and by monitoring. But standalone Docker doesn't restart an unhealthy container by itself.Keep the check cheap, make sure the tool it calls (
curl,wget) exists in the image, and remember that Kubernetes ignoresHEALTHCHECKand uses its own liveness and readiness probes.FROM nginx:alpine HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD wget -q --spider http://localhost/ || exit 1What interviewers listen for- Exit 0 is healthy, 1 is unhealthy
- Status: starting, healthy, unhealthy
- Defaults: 30s interval, 30s timeout, 3 retries
--start-periodgives slow apps a grace period- Standalone Docker does not restart unhealthy containers
Likely follow-up: How would you health-check a distroless image with no shell or curl?
23.Why should containers run as a non-root user, and how do you set that up?mid
By default the process in a container runs as root (UID 0). It's namespaced and has a reduced set of capabilities, but without user namespaces it's still UID 0 on the shared kernel. If an attacker escapes through a kernel or runtime bug, or through a dangerous mount like the Docker socket, they're root on the host. Even without an escape, root inside lets a compromised app rewrite its own files and install tools.
To run as non-root:
- Create a dedicated user, or use one the base image provides, like
nodein the official Node images - Switch with
USER, ideally by numeric UID, so Kubernetes'runAsNonRootcheck can verify it - Use
COPY --chownonly for paths the app must write; keep code root-owned and read-only - Override at run time if needed with
docker run --user 10001:10001
The usual pain point is bind mounts and volumes: the container's UID needs permission on the mounted files, so align UIDs or fix ownership deliberately.
FROM alpine:3.20 RUN addgroup -S app && adduser -S -G app -u 10001 app WORKDIR /app COPY --chown=app:app ./bin/server /app/server USER 10001 CMD ["/app/server"]What interviewers listen for- Containers run as root by default
- Container root is host root without user namespaces
- Create a user and switch with USER
- Prefer a numeric UID for policy checks
- Mounted files need matching UID permissions
Likely follow-up: What is the difference between running as non-root and rootless mode?
- Create a dedicated user, or use one the base image provides, like
24.How are containers actually implemented on Linux? Explain the role of namespaces and cgroups.hard
A container isn't a kernel object. It's an ordinary process that the runtime starts with two Linux features applied.
Namespaces control what the process can see:
- pid: its own process tree, where the app is PID 1
- net: its own interfaces, IP addresses, routes and ports
- mnt: its own mount table and root filesystem
- uts: its own hostname; ipc: its own shared memory and message queues
- user: maps container UIDs to other host UIDs; Docker uses it only in rootless mode or with
userns-remap - cgroup: hides the host's cgroup hierarchy
Control groups (cgroups) control what it can use: memory, CPU time, block I/O and the number of processes, with the accounting that
docker statsreads.On top of that, Docker drops most Linux capabilities, applies a default seccomp profile that blocks risky system calls, and can apply AppArmor or SELinux policies. All of it relies on one shared kernel, so a kernel vulnerability can undermine every layer.
What interviewers listen for- A container is a process, not a kernel object
- Namespaces limit what it can see
- cgroups limit what it can use
- Capabilities, seccomp and LSMs add hardening
- The shared kernel is the weak point
Likely follow-up: Which capabilities does Docker keep by default, and why is
--privilegeddangerous?25.How do you limit a container's memory and CPU, and what happens when it exceeds those limits?mid
By default a container has no resource limits: it can use as much memory and CPU as the host's kernel scheduler allows, so one leaky service can starve everything else. Docker enforces limits with cgroups:
--memory(-m) is a hard limit. Go over it and the kernel's OOM killer kills a process in the container, usually the main one, giving exit code 137 andOOMKilled: true--memory-swapis memory plus swap; setting it equal to--memorymeans no swap--cpus 1.5caps CPU time at one and a half CPUs' worth. Going over means throttling, not killing--cpu-sharesis a relative weight that only matters when CPUs are contended--pids-limitcaps the number of processes, which stops fork bombs
In Compose, set them under
deploy.resources.limits. Make sure the runtime respects the limit: modern JVMs size their heap from the cgroup limit, but other runtimes may need explicit heap flags. Usedocker statsto size limits from real usage.What interviewers listen for- No limits by default
- Exceeding memory triggers the OOM killer, exit 137
- CPU limits throttle rather than kill
--memory-swapequal to--memorydisables swap- Enforced by cgroups; observe with
docker stats
Likely follow-up: Why might a Java app get OOM-killed even though its heap is below the limit?
26.Why does
docker stopsometimes hang for 10 seconds and then kill the app? What is special about PID 1 in a container?hardThe first process in a container is PID 1 in its PID namespace, and the kernel treats PID 1 specially: signals whose default action is to terminate, like
SIGTERMandSIGINT, are ignored unless the process installs a handler. PID 1 also adopts orphaned processes and is expected to reap zombies.docker stopsendsSIGTERM, waits a grace period (10 seconds by default), then sendsSIGKILL. If PID 1 ignoresSIGTERM, you get a slow stop, exit code 137 and no clean shutdown. Common causes:- Shell-form
CMDorENTRYPOINT, where/bin/sh -cis PID 1 and doesn't forward the signal - An entrypoint script that starts the app without
exec - An app with no
SIGTERMhandler
Fixes: use exec form, end entrypoint scripts with
exec "$@", handleSIGTERMin the app (stop accepting work, drain requests, close connections, exit), and add a minimal init like tini withdocker run --init. The init runs as PID 1, forwards signals to your app and reaps zombies.# Problem: shell form, so sh is PID 1 and node never gets SIGTERM CMD node server.js && echo done # Fix 1: exec form, node is PID 1 and must handle SIGTERM CMD ["node", "server.js"] # Fix 2: a tiny init as PID 1 that forwards signals and reaps zombies # docker run --init my-image (Compose: init: true)What interviewers listen for- PID 1 ignores default-action signals without a handler
- docker stop: SIGTERM, grace period, then SIGKILL
- Shell form makes sh PID 1
- Entrypoint scripts should
exec "$@" --initruns tini to forward signals and reap zombies
Likely follow-up: What is a zombie process, and why might they pile up in a container?
- Shell-form
27.What is the difference between shell form and exec form for
RUN,CMDandENTRYPOINT?midShell form is a plain string, like
CMD node server.js. Docker runs it as/bin/sh -c "node server.js", so you get shell features: variable expansion,&&, pipes and redirects.Exec form is a JSON array, like
CMD ["node", "server.js"], and Docker executes the binary directly, without a shell.Why it matters:
- Signals: in shell form the shell can end up as PID 1 and not forward
SIGTERM. Exec form makes your app PID 1 - Variables: exec form does no substitution, so
CMD ["echo", "$HOME"]prints the literal$HOME. Use["sh", "-c", "..."]when you need expansion - ENTRYPOINT: shell form ignores
CMDanddocker runarguments - Minimal images: shell form needs
/bin/sh, whichscratchand distroless images lack - Syntax: exec form must be valid JSON with double quotes; with single quotes, Docker silently treats the line as shell form
Rule of thumb: shell form for
RUN, exec form forCMDandENTRYPOINT.# Shell form: runs via /bin/sh -c, so variables and && work RUN apt-get update && apt-get install -y curl CMD echo "Hello $NAME" # Exec form: a JSON array, executed directly with no shell CMD ["node", "server.js"] CMD ["sh", "-c", "echo Hello $NAME"] # explicit shell when you need oneWhat interviewers listen for- Shell form runs via
/bin/sh -c - Exec form runs the binary directly
- Exec form makes the app PID 1 for signals
- Exec form does no variable expansion
- Exec form must be valid JSON with double quotes
Likely follow-up: How would you use an environment variable in an exec-form
CMD?- Signals: in shell form the shell can end up as PID 1 and not forward
29.What is a container registry, and how do you push an image to a private one?easy
A registry stores and distributes images, organised into repositories that hold tagged versions. Docker Hub is the default, and common alternatives are Amazon ECR, Google Artifact Registry, Azure Container Registry, GitHub Container Registry and self-hosted Harbor.
An image reference looks like
registry/namespace/repository:tag. Missing parts get defaults, songinx:alpinereally meansdocker.io/library/nginx:alpine.The workflow:
docker loginauthenticates to the registrydocker taggives the local image a name that includes the target registrydocker pushuploads only the layers the registry doesn't already havedocker pulldownloads only the layers you're missing
Registries implement the OCI Distribution API, so any compliant tool can push and pull. In production, teams use a private registry close to their cluster, turn on vulnerability scanning there, and mirror or cache public base images to avoid rate limits and outages.
What interviewers listen for- Registries store images in repositories
- Docker Hub is the default registry
- Reference format: registry/namespace/repo:tag
- Push and pull transfer only missing layers
- Use private registries and mirror public bases
Likely follow-up: How would a CI pipeline authenticate to a registry without a long-lived password?
30.Why is passing a database password as an environment variable risky, and what should you do instead?mid
Environment variables are fine for configuration like log levels, feature flags and hostnames, but they're a poor home for secrets:
- Anyone who can run
docker inspecton the container can read them - Every child process inherits them, and they tend to leak into logs, crash reports and debug endpoints
- Values set with
ENVin a Dockerfile are baked into the image for anyone who pulls it
A secret is delivered as a file instead. In Compose and Swarm, a granted secret is mounted at
/run/secrets/<name>in that service's containers and isn't stored in the image. Many official images support a_FILEconvention, such asPOSTGRES_PASSWORD_FILE, so the value is read from that file.In production, secrets usually come from a manager like Vault, AWS Secrets Manager or Kubernetes Secrets and are injected at run time. For build-time secrets, such as a private package token, use BuildKit's
RUN --mount=type=secretso the value never lands in a layer.What interviewers listen for- Env vars are fine for non-sensitive config
- Env vars leak via inspect, logs and child processes
- Secrets are mounted as files in
/run/secrets _FILEconvention in many official images- Build-time secrets via BuildKit secret mounts
Likely follow-up: How are Swarm secrets stored and delivered differently from Compose file secrets?
- Anyone who can run
31.How do you choose between full,
-slim, Alpine and distroless base images?midThe base image is usually the biggest factor in both size and attack surface:
- Full Debian or Ubuntu tags like
node:22orpython:3.12: compilers and common libraries included. Easiest to work with, but large and with many packages to patch. Good for build stages -slimvariants: the same distro and glibc, minus most extras. Usually the best default for runtime stages, because almost everything still just works- Alpine: very small, using musl libc and BusyBox instead of glibc and GNU tools. The catch is compatibility: native binaries and language packages built for glibc may not work, may need compiling from source (slow builds), or may behave subtly differently
- Distroless: just the language runtime and its dependencies, with no shell or package manager. Tiny attack surface and fewer CVEs, but harder to debug; distroless offers
:debugvariants with a BusyBox shell scratch: completely empty, ideal for static Go or Rust binaries
My default is a full image to build, slim or distroless to run, and Alpine only once I've confirmed the app works on musl.
What interviewers listen for- Full images: convenient but large
- Slim: glibc compatibility at a fraction of the size
- Alpine: tiny, but musl can break native dependencies
- Distroless: no shell or package manager
scratchfor static binaries
Likely follow-up: Why can a Python image take much longer to build on Alpine?
- Full Debian or Ubuntu tags like
32.What happens, step by step, when you run
docker run -d -p 8080:80 nginx?midDocker is client-server. The
dockerCLI sends REST API calls to the Docker daemon,dockerd, usually over the Unix socket/var/run/docker.sock. Then:- The daemon looks for
nginx:latestlocally and, if it's missing, pulls it: resolves the tag to the manifest for the host's platform and downloads the missing layers - It creates the container: a writable layer on top of the image layers, plus its config for env, mounts and restart policy
- It sets up networking: attaches the container to the default bridge, assigns an IP and adds NAT rules so host port 8080 forwards to container port 80
- It asks containerd to start it. containerd launches a shim process, which calls runc
- runc creates the namespaces and cgroups, sets up the root filesystem, executes the entrypoint and command, and exits. The shim stays as the container's parent, holding its stdio and exit status
stdout and stderr go to the logging driver, and since we passed
-d, the CLI just prints the container ID.What interviewers listen for- CLI calls the dockerd REST API over a socket
- Pulls missing layers for the host platform
- Creates writable layer, network and port mapping
- containerd and a per-container shim manage it
- runc creates namespaces and cgroups, then exits
Likely follow-up: How can containers keep running while
dockerdrestarts?- The daemon looks for
33.What is the difference between
docker execanddocker attach?easydocker execstarts a new process inside an already running container, sharing its namespaces, filesystem and network.docker exec -it api shis the standard way to get a shell for debugging, and exiting that shell doesn't affect the main process. It only works on running containers.docker attachconnects your terminal to the main process's stdin, stdout and stderr. No new process is started: you see the app's output, and what you type goes to it. The catch is that Ctrl+C is forwarded to the main process and can stop the container. To detach from a container started with-it, press Ctrl+P then Ctrl+Q.docker runis different again: it creates a new container from an image.In practice:
execto investigate,logs -fto watch output, andattachonly for interactive programs like a REPL or console that expects input.What interviewers listen for- exec starts a new process in a running container
- attach connects to the main process stdio
- Ctrl+C while attached can stop the container
- Detach with Ctrl+P then Ctrl+Q
- run creates a new container instead
Likely follow-up: How do you get a shell in a container that has already crashed?
34.How does logging work in Docker, and how do you stop container logs from filling the disk?easy
Docker captures whatever the main process writes to stdout and stderr, which is why containerised apps should log to the console rather than to files.
docker logsreads it back, with-fto follow,--tail,--sinceand-tfor timestamps.Where logs end up depends on the logging driver:
json-fileis the default. It writes JSON lines on the host and does no rotation by default, so a chatty container can fill the disk. Set themax-sizeandmax-fileoptions, or switch to thelocaldriver, which Docker recommends because it rotates by default and uses a more compact format- Other drivers ship logs elsewhere:
syslog,journald,fluentd,awslogs,gcplogs,splunkand more - With remote drivers, dual logging keeps a local cache so
docker logsstill works
Set the default in
daemon.json, or per container with--log-driverand--log-opt. In production, most teams ship logs to a central system like ELK, Loki or CloudWatch instead of reading them on hosts.What interviewers listen for- Apps should log to stdout and stderr
docker logs -f,--tail,--since- Default json-file driver does not rotate
- Set max-size/max-file or use the local driver
- Ship logs to a central system in production
Likely follow-up: Why does changing the logging driver in
daemon.jsonnot affect existing containers?35.Your build server has run out of disk space because of Docker. How do you find what's using it and clean up safely?easy
Docker doesn't clean up unused objects on its own, so stopped containers, old images, build cache and volumes pile up. Start with
docker system df, or-vfor detail, to see which category is the problem.Then use the matching
prunecommand:docker container pruneremoves stopped containersdocker image pruneremoves only dangling images, the untagged<none>images left behind when a tag moves to a new build. Add-ato remove every image not used by a containerdocker builder pruneclears the build cache, often the biggest item on CI machinesdocker system prunedoes containers, unused networks, dangling images and build cache in one go
Volumes are never removed by default, because they hold data.
docker volume pruneremoves unused anonymous volumes, and-aincludes named ones, so check before running it.On CI, prune with
--filter until=24hto keep recent cache, and fix the causes: run containers with--rm, rotate logs and limit the builder's cache size.docker system df # images, containers, volumes, build cache docker container prune # stopped containers docker image prune # dangling images only docker image prune -a # every image not used by a container docker builder prune # build cache docker system prune # containers, networks, dangling images, build cache docker volume prune # unused anonymous volumes (-a for named ones too)What interviewers listen fordocker system dfshows usage by type- image prune removes dangling;
-aall unused docker builder prunefor build cache- Volumes are never pruned by default
- Use filters to keep recent objects
Likely follow-up: What is a dangling image, and how is it created?
36.Explain the common
docker runflags:-d,-it,--rm,--name,-e,-pand-v.easy-d(detached) runs the container in the background and prints its ID. Without it, your terminal is attached to the container's output-ikeeps stdin open and-tallocates a pseudo-terminal. Together,-itgives you an interactive shell--rmremoves the container when it exits, perfect for one-off commands so stopped containers don't pile up--namesets a fixed, readable name instead of a random one. Names must be unique, and on user-defined networks they double as DNS names-e KEY=valuesets an environment variable, and--env-fileloads many from a file-p host:containerpublishes a container port on the host-v source:targetmounts a named volume or host path;--mountis the more explicit equivalent
Anything after the image name replaces the image's
CMD, sodocker run --rm nginx:alpine nginx -vprints the nginx version instead of starting the server.What interviewers listen for-druns in the background-itfor an interactive terminal--rmremoves the container on exit-e,-p,-vfor env, ports and storage- Arguments after the image replace CMD
Likely follow-up: What happens if you run
docker run -d ubuntuwith no command?37.How would you harden a container image and its runtime configuration for production?hard
I think about it in two halves.
The image:
- A minimal base (slim or distroless) and multi-stage builds, so there's less to exploit and patch
- A non-root user with a numeric UID
- Scan in CI and in the registry with a tool like Docker Scout, Trivy or Grype, and rebuild regularly to pick up base-image fixes
- Pin versions or digests, generate an SBOM, and sign images (for example with cosign) so deployments can verify them
- No secrets in layers; use BuildKit secret mounts
The runtime, following least privilege:
--cap-drop ALLand add back only what's needed- A
--read-onlyroot filesystem, withtmpfsfor scratch paths --security-opt no-new-privilegesto block privilege escalation through setuid binaries- Keep the default seccomp and AppArmor or SELinux profiles, and never use
--privilegedor mount/var/run/docker.sockcasually - Resource limits, and publish only the ports you need
For another layer of isolation, run the daemon in rootless mode or enable user namespace remapping.
docker run -d --name api \ --user 10001:10001 \ --read-only --tmpfs /tmp \ --cap-drop ALL --cap-add NET_BIND_SERVICE \ --security-opt no-new-privileges \ --memory 512m --pids-limit 200 \ my-api:1.4.0What interviewers listen for- Minimal base images and non-root users
- Scan, pin and sign images
- Drop all capabilities, add back only what is needed
- Read-only root filesystem with tmpfs
- Avoid
--privilegedand the Docker socket
Likely follow-up: A scanner reports 200 CVEs in your image. How do you triage them?
38.A container exited with code 137. What does that mean, and what about 125, 126, 127 and 143?easy
Codes above 128 mean the process was killed by a signal: the code is 128 plus the signal number.
- 0: the process finished successfully
- 1, or another small number: the application reported an error itself; check
docker logs - 125: the
docker runcommand itself failed, for example because of an invalid flag, so no container process ran - 126: the command was found but couldn't be executed, such as a file without execute permission
- 127: command not found, often a typo or a binary missing from a slim image
- 137 = 128 + 9, SIGKILL: the OOM killer (
docker inspectshowsOOMKilled: true),docker kill, ordocker stopgiving up after its grace period - 143 = 128 + 15, SIGTERM: the process was terminated by the signal
docker stopsends. An app that handlesSIGTERMitself usually exits with 0 instead - 139 = 128 + 11, a segmentation fault
So 137 sends me straight to memory limits and to whether the app handles
SIGTERMquickly enough.What interviewers listen for- Above 128 means killed by signal (128 + n)
- 137 is SIGKILL: OOM, kill or stop timeout
- 143 is SIGTERM
- 125 is a Docker error, 126 not executable
- 127 is command not found
Likely follow-up: How do you confirm that a 137 was caused by the OOM killer?
39.Your app runs fine in a container, but
curl localhost:8080on the host gets "connection refused". What do you check?easyI'd go through these in order:
- Is the port published?
docker psordocker port <name>should show something like0.0.0.0:8080->3000/tcp.EXPOSEalone doesn't publish; you need-p 8080:3000 - Is the mapping the right way round? It's
-p HOST:CONTAINER. Mapping to a container port the app doesn't listen on gives exactly this symptom - Which address does the app bind to? The classic one. If the app listens on
127.0.0.1inside the container, it only accepts connections from inside that container's network namespace. Published traffic arrives on the container's network interface, so the app must listen on0.0.0.0. Many dev servers default to localhost and need a--host 0.0.0.0flag - Is it running and listening? Check
docker logs, and test from inside withdocker exec - Anything in between? A host firewall, the host port already in use, or on Docker Desktop, trying the container's IP directly, which isn't routable from macOS or Windows
And remember:
localhostinside a container means the container itself, not the host.What interviewers listen for- Check the port is published with
-p - The mapping is HOST:CONTAINER
- App must listen on 0.0.0.0, not 127.0.0.1
- Test from inside with
docker exec - Container localhost is not the host
Likely follow-up: How would a container reach a database running on the host machine?
- Is the port published?
40.Should you run several processes, like nginx, your app and cron, in one container?easy
The guideline is one concern per container. Usually that's one main process, though a process that forks its own workers, like nginx or gunicorn, is fine.
Reasons to split them:
- Lifecycle: Docker only watches PID 1. If a background process dies, the container keeps running and nobody notices; if PID 1 dies, everything goes down with it
- Scaling: you can scale the web tier without also scaling cron
- Logs and signals: each container's stdout is one clean stream, and
docker stopshuts down one thing properly - Updates and reuse: rebuild and redeploy each piece independently, and use the official nginx or Redis images instead of maintaining your own bundle
Run the pieces as separate containers and connect them with a network, Compose or a Kubernetes pod, where tightly coupled helpers run as sidecars.
When you genuinely need several processes, for example to wrap a legacy app, use a proper init or supervisor, such as tini with a small script or
supervisord, so signals are forwarded and zombies reaped. Treat it as the exception.What interviewers listen for- One concern per container
- Docker only monitors PID 1
- Independent scaling, logs and updates
- Group related containers with Compose or pods
- Use a supervisor if multiple processes are unavoidable
Likely follow-up: What is a sidecar container, and when would you use one?
41.What is the difference between
docker saveanddocker export?midThey work on different objects and produce different archives:
docker savetakes one or more images and writes a tar with all their layers, config and tags.docker loadrestores them exactly, with history,CMD,ENVand tags intact. It's the way to move images to an air-gapped machine or cache them without a registrydocker exporttakes a container and writes its flattened filesystem: the image layers plus the container's changes, merged into one tree, with no layers, history or metadata.docker importturns that tar into a new single-layer image with noCMD,ENVor entrypoint unless you add them with--change
Neither includes volume contents; volume data lives outside the container's filesystem.
So:
saveandloadto transport images faithfully,exportandimportto snapshot or flatten a filesystem, which is rarely what you want for deployments. Pushing to a registry is usually better than either.What interviewers listen for- save works on images, export on containers
- save keeps layers, tags and config
- export produces one flattened filesystem
- import creates a single-layer image without metadata
- Volume data is not included
Likely follow-up: How would you move images to a server with no internet access?
42.How does Podman differ from Docker?mid
Both build and run OCI containers with a nearly identical CLI:
podman runtakes the same flags asdocker run, and images move freely between them. The differences are architectural:- Daemon: the Docker CLI talks to a long-running daemon,
dockerd, which traditionally runs as root. Podman is daemonless: eachpodmancommand launches the container itself, with a small monitor process,conmon, per container - Rootless: Podman was designed to run rootless for ordinary users from the start. Docker supports rootless mode too, but as an opt-in setup
- Pods: Podman can group containers into pods that share a network namespace, like Kubernetes, and can generate or play Kubernetes YAML
- systemd: Podman integrates with systemd, via Quadlet unit files, to run containers as services, instead of relying on a daemon and restart policies
- Ecosystem: Docker bundles Docker Desktop, Compose, Buildx and Hub. Podman provides a Docker-compatible API socket,
podman compose, and Buildah for builds
I'd pick Docker for developer experience and tooling, and Podman where daemonless, rootless operation or Red Hat ecosystem integration matters.
What interviewers listen for- Same OCI images and a near-identical CLI
- Podman is daemonless; Docker uses dockerd
- Podman is rootless-first
- Podman pods mirror Kubernetes pods
- systemd integration via Quadlet
Likely follow-up: Can you run a Compose file with Podman?
- Daemon: the Docker CLI talks to a long-running daemon,
43.What are OCI, containerd and runc, and how do they relate to Docker?hard
The Open Container Initiative (OCI) publishes open standards so tools can interoperate:
- Image spec: how an image is laid out, with a manifest, config and content-addressed layers
- Runtime spec: how to run a container from a filesystem bundle and a
config.json - Distribution spec: the HTTP API registries use for push and pull
Docker's stack is layered on top of these:
dockerdprovides the Docker API, BuildKit builds, networking and volumes- containerd is a high-level runtime: it pulls and stores images, manages snapshots and supervises container lifecycles
- A shim per container keeps it running independently of the daemons and reports its exit status
- runc is the low-level OCI runtime: it creates namespaces and cgroups, starts the process, then exits. Alternatives like crun, gVisor's runsc or Kata Containers plug in here
That's why Kubernetes could remove dockershim in v1.24: kubelet talks to containerd or CRI-O through the CRI, and Docker-built images still run because they're OCI images.
What interviewers listen for- OCI defines image, runtime and distribution specs
- dockerd provides the API and developer features
- containerd manages images and container lifecycle
- runc creates namespaces and cgroups per the spec
- Docker-built images run on any OCI runtime
Likely follow-up: What changed for Docker users when Kubernetes removed dockershim?
44.Your build needs a private npm token. Why is
ARG NPM_TOKENa bad idea, and how should you pass it instead?hardBuild args and
ENVvalues are recorded in the image: build-arg values show up indocker history, andENVis stored in the image config. Copying an.npmrcin and deleting it later doesn't help either, because the earlier layer still contains the file. Anyone who can pull the image can recover the token.BuildKit secret mounts fix this. The secret is mounted as a file for a single
RUNstep, at/run/secrets/<id>by default or wherevertargetpoints, and it's never written to a layer or the build cache. You supply it at build time:- from a file:
docker build --secret id=npmrc,src=$HOME/.npmrc . - from an environment variable:
--secret id=npm_token,env=NPM_TOKEN
In the Dockerfile,
RUN --mount=type=secret,id=npm_token,env=NPM_TOKENexposes it as an environment variable for that one command instead of a file.For Git over SSH, use
RUN --mount=type=sshwithdocker build --ssh default, which forwards your SSH agent instead of copying keys. Compose supports the same throughbuild.secrets.# syntax=docker/dockerfile:1 FROM node:22-bookworm-slim WORKDIR /app COPY package.json package-lock.json ./ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci COPY . . # docker build --secret id=npmrc,src=$HOME/.npmrc .What interviewers listen for- Build args and ENV leak into image metadata
- Deleting a file later leaves it in a layer
RUN --mount=type=secretexposes it to one step only- Pass it with
docker build --secret - Use
--mount=type=sshfor SSH keys
Likely follow-up: How would you verify that a secret did not end up in the final image?
- from a file:
45.What is a BuildKit cache mount, and how is it different from the normal layer cache?hard
The layer cache is all or nothing: if
package-lock.jsonchanges, thenpm cilayer is rebuilt from scratch and every package is downloaded again.A cache mount gives a
RUNstep a persistent directory that survives between builds but is not part of the image. Point it at the package manager's cache, like/root/.npm,/root/.cache/pip,/go/pkg/modor/root/.m2, and a rebuild only downloads what changed.Key properties:
- Its contents never end up in a layer, so no cleanup commands are needed and the image stays small
- The layer cache is still checked first; a cache mount only speeds up steps that actually re-run
- The
sharingoption (shared,privateorlocked) controls concurrent builds;lockedis common for apt, which can't handle parallel access - It lives in the builder's local cache, so
docker builder pruneclears it, and it isn't included when you export layer cache to a registry, which limits its value on ephemeral CI runners
Cache mounts are a BuildKit feature, and BuildKit is the default builder in current Docker releases.
FROM node:22-bookworm-slim WORKDIR /app COPY package.json package-lock.json ./ RUN --mount=type=cache,target=/root/.npm \ npm ci COPY . .What interviewers listen for- Persistent cache directory for a RUN step
- Contents never end up in image layers
- Speeds up re-runs after dependency changes
sharing=lockedfor tools like apt- Local to the builder, not exported with layer cache
Likely follow-up: How would you cache apt downloads safely with a cache mount?
46.How does Docker store image and container filesystems, and what is copy-on-write?hard
Docker uses a union filesystem, typically overlayfs. The image's read-only layers are stacked as lower directories, each container gets a thin writable upper layer, and the container sees the merged view.
Copy-on-write means files are copied only when they change:
- Reads come straight from whichever layer holds the file, so many containers share one copy of the image
- The first write to an existing file copies it up into the writable layer, then modifies the copy, which is slow for large files
- Deleting a file that lives in a lower layer creates a whiteout marker in the upper layer; the original bytes are still in the image
So containers start fast because nothing is copied up front, write-heavy data like databases belongs on volumes, which bypass the storage driver, and deleting files in a later Dockerfile instruction never shrinks an image.
Older installs use the
overlay2storage driver; fresh installs of Docker Engine 29 and later use the containerd image store with its overlayfs snapshotter, which follows the same model.What interviewers listen for- Read-only image layers plus a writable container layer
- overlayfs merges layers into one view
- The first write copies the file up
- Deletes create whiteouts, not real removals
- Write-heavy data belongs in volumes
Likely follow-up: Why is a database on the container writable layer slower than on a volume?
47.Why is running a container with
--privileged, or mounting/var/run/docker.sockinto it, dangerous?hardBoth effectively remove the container boundary.
--privilegedgives the container all Linux capabilities and access to all host devices, and lifts the seccomp and AppArmor or SELinux confinement Docker normally applies. A root process in that container can mount the host's disks or change kernel settings, so escaping to the host is trivial. It's occasionally needed, for Docker-in-Docker or low-level system tools, but should be treated like running directly on the host.Mounting the Docker socket hands the container full control of the Docker API. Anything that can talk to the socket can start a new privileged container with the host's root filesystem mounted and take over the machine. It's root-equivalent, even if the process using it isn't root.
Safer alternatives:
- Add only the specific capability (
--cap-add) or device (--device) you need - For CI image builds, use a rootless BuildKit or a remote builder instead of sharing the host's daemon
- If a tool genuinely needs the API, put a filtering socket proxy in front and allow only the endpoints it needs
What interviewers listen for--privilegedgrants all capabilities and devices- It lifts seccomp and AppArmor/SELinux confinement
- Docker socket access is root on the host
- Use targeted
--cap-addor--deviceinstead - Use rootless or remote builders in CI
Likely follow-up: What is the difference between Docker-in-Docker and mounting the host socket?
- Add only the specific capability (
48.What is Docker rootless mode, and how is it different from just setting
USERin the Dockerfile?hardThey're separate layers of defence. Setting
USERmakes the app run as a non-root user inside the container, which limits what it can do, but the Docker daemon still runs as root.Rootless mode runs the daemon itself and all its containers as an unprivileged user, inside a user namespace. Root in a container maps to your ordinary host user, so even a full escape only gets that user's privileges. You set it up with
dockerd-rootless-setuptool.sh install; it needsnewuidmapandnewgidmapplus subordinate ID ranges in/etc/subuidand/etc/subgid, and it gets its own socket and CLI context.Limitations to know:
- Publishing host ports below 1024 needs extra configuration
- Some features are unsupported, including AppArmor, checkpointing and overlay networks
- Resource limits require cgroup v2 with systemd
- Networking goes through RootlessKit, which adds some overhead
It also differs from
userns-remap, where the daemon stays root but container UIDs are mapped to unprivileged host UIDs. Podman takes the rootless approach by default.What interviewers listen for- USER alone still leaves a root daemon
- Rootless runs the daemon and containers unprivileged
- Relies on user namespaces and subordinate UIDs
- Limits: low ports, some features, cgroup v2
- userns-remap keeps the daemon as root
Likely follow-up: What does root inside a rootless container map to on the host?
49.In Compose, what is the difference between the
.envfile andenv_file:? Which value wins if a variable is set in several places?midThey solve different problems:
- The
.envfile in the project directory is read by Compose itself for interpolation: it fills in${TAG}style references incompose.yaml. Its values are not automatically passed into containers env_file:on a service lists files whose variables are injected into that service's containersenvironment:sets container variables inline, and can use interpolated values
When a container variable is set in several places, the order of precedence is:
docker compose run -eon the command line, thenenvironment:, thenenv_file:, then the image'sENV. For interpolation, variables from your shell override those in.env.To check,
docker compose configprints the fully resolved file, anddocker compose exec <service> envshows what the container actually sees. Keep.envfiles with real credentials out of Git, and don't confuse either mechanism with Composesecrets, which mount files instead of setting variables.# .env, next to compose.yaml: only used for ${...} substitution TAG=16-alpine # compose.yaml services: db: image: postgres:${TAG} env_file: db.env # injected into the container environment: POSTGRES_DB: app # also injected; wins over env_fileWhat interviewers listen for.envfeeds${VAR}interpolation in the Compose fileenv_fileinjects variables into containers- Precedence: CLI, environment, env_file, image ENV
- Shell variables override
.envfor interpolation - Verify with
docker compose config
Likely follow-up: How would you keep separate settings for dev and production with Compose?
- The
50.An image built on an Apple Silicon Mac fails in production with
exec format error. Why, and how do you fix it?hardImages are built for a specific CPU architecture. An Apple Silicon Mac builds
linux/arm64images by default, while most x86 servers needlinux/amd64. When the kernel is asked to execute a binary for the wrong architecture, it fails withexec format error.Fixes:
- Build for the target explicitly:
docker build --platform linux/amd64 . - Better, publish a multi-platform image. The tag then points to an image index listing one manifest per platform, and each host pulls the variant that matches it
- Building for a foreign architecture uses QEMU emulation, which is slow for heavy compile steps. For compiled languages, cross-compile instead: run the build stage with
FROM --platform=$BUILDPLATFORMand use the automaticTARGETOSandTARGETARCHbuild args to choose the output - Or build natively on CI runners of each architecture and combine the results
Building multi-platform images with the default builder needs the containerd image store, the default on fresh recent installs, or a
docker-containerbuilder.docker buildx imagetools inspectshows which platforms a tag supports.What interviewers listen for- Images are built per CPU architecture
exec format errormeans an architecture mismatch- Use
--platformor a multi-platform build - An image index maps one tag to per-platform manifests
- Cross-compile with BUILDPLATFORM and TARGETARCH
Likely follow-up: Why is emulated building slow, and how does cross-compilation avoid it?
- Build for the target explicitly:
51.What do
WORKDIRandLABELdo, and why isRUN cd /appnot the same asWORKDIR /app?easyWORKDIRsets the working directory for the instructions that follow, such asRUN,CMD,ENTRYPOINT,COPYandADD, and for the running container. If the directory doesn't exist, it's created. A relative path is resolved against the previousWORKDIR, and you can use it as many times as you like.RUN cd /appdoesn't persist: eachRUNstarts a fresh shell, so thecdonly applies inside that one command. That's why Dockerfiles that rely oncdend up with longcd x && ...chains, and whyWORKDIRwith absolute paths is the clearer, recommended approach.LABELadds key-value metadata to the image config without adding any files. Common uses are the standard OCI keys likeorg.opencontainers.image.source,versionandrevision, plus team or ownership labels. You can read them withdocker inspectand filter with--filter label=..., and registries and scanners use them to link an image back to its source.LABELreplaces the deprecatedMAINTAINERinstruction.FROM alpine:3.20 LABEL org.opencontainers.image.source="https://github.com/acme/api" \ org.opencontainers.image.version="1.4.0" WORKDIR /app RUN cd /tmp && touch scratch # the cd only lasts for this RUN RUN pwd # prints /appWhat interviewers listen for- WORKDIR sets the directory for later instructions
- It creates the directory if missing
RUN cdonly lasts for that one RUN- LABEL adds metadata without adding files
- Use OCI label keys; MAINTAINER is deprecated
Likely follow-up: How would you stamp the Git commit into an image automatically?
52.What are Compose profiles, and when would you use them?mid
Profiles let one Compose file contain optional services that only start when you ask for them. A service with a
profilesattribute is enabled only when one of its profiles is active; services withoutprofilesare always enabled.You activate profiles with:
docker compose --profile debug up, repeatable for several profiles- the
COMPOSE_PROFILES=debug,toolsenvironment variable
Explicitly targeting a service also starts it even if its profile isn't active, so
docker compose run --rm seedworks for a one-off job.Typical uses are debugging tools like database admin UIs, test runners, seed or migration jobs, and heavy optional dependencies. The default
docker compose upstays fast and lean while everything lives in one file.Two cautions: make sure anything an always-on service depends on is itself always on, and for bigger differences, like dev versus production settings, multiple files merged with
-for theincludeelement usually fit better than profiles.What interviewers listen for- Profiles make services optional
- Services without profiles always start
- Activate with
--profileor COMPOSE_PROFILES - Targeting a service by name starts it
- Good for debug tools, seeders and test runners
Likely follow-up: How would you override settings for production without duplicating the Compose file?
53.How does a container connect to a service running on the host machine, such as a database on the host's port 5432?mid
localhostinside a container is the container itself, solocalhost:5432won't reach the host. The options:- Docker Desktop (macOS and Windows) provides the special DNS name
host.docker.internal, which resolves to the host - Docker Engine on Linux doesn't define it by default, but you can add it with
--add-host=host.docker.internal:host-gateway, orextra_hostsin Compose.host-gatewayis a special value that Docker replaces with the host's address - Host networking (
--network host) on Linux puts the container in the host's network stack, solocalhostreally is the host, at the cost of isolation
Don't forget the host side. On Linux, a service bound only to
127.0.0.1won't accept connections arriving from the Docker bridge, so it may need to listen on the bridge address or all interfaces, and the host firewall has to allow it.If the host service is really part of your stack, the cleaner long-term fix is to run it as a container on the same user-defined network.
# Docker Desktop: the name exists out of the box docker run --rm alpine:3.20 nc -z host.docker.internal 5432 # Docker Engine on Linux: map the name to the host gateway yourself docker run --rm --add-host=host.docker.internal:host-gateway alpine:3.20 \ nc -z host.docker.internal 5432What interviewers listen for- Container localhost is not the host
- Docker Desktop provides
host.docker.internal - On Linux use
--add-hostwithhost-gateway - Host networking shares the host stack on Linux
- The host service must listen on a reachable interface
Likely follow-up: How would you do the same thing in a Compose file?
- Docker Desktop (macOS and Windows) provides the special DNS name
54.A container running as a non-root user gets "permission denied" when writing to a bind-mounted directory. Why, and how do you fix it?hard
Bind mounts pass host files straight through, and the kernel checks permissions by numeric UID and GID, not by user name. If the host directory is owned by UID 1000 and the container process runs as UID 10001, the kernel sees a different user and denies the write.
Fixes, depending on the situation:
- Run the container as the owning UID with
--user "$(id -u):$(id -g)", common for dev tooling - Change the host directory's ownership or permissions to match the container's UID, or use a shared group
- Build the image with a UID that matches your deployment convention, passed as a build arg
- Use a named volume instead; when Docker populates an empty volume from the image, it keeps the image's ownership
- On SELinux hosts like Fedora or RHEL, the denial may come from labels, not UIDs; add
:zor:Zto the mount
Avoid
chmod 777or running as root, which swaps a permissions problem for a security one. With rootless Docker or user namespaces, UIDs are shifted too, so check the mapping.What interviewers listen for- Permissions are checked by numeric UID and GID
- Container and host UIDs must line up
- Use
--useror fix host ownership - Named volumes keep the image ownership
- SELinux hosts may need
:zor:Z
Likely follow-up: Why do files created by a container in a bind mount end up owned by root on the host?
- Run the container as the owning UID with
55.How do you debug a running container built from a distroless or
scratchimage that has no shell?harddocker exec -it api shfails because there's noshin the image. Options, from least to most invasive:- Look from outside first:
docker logs,docker inspect,docker top,docker stats, anddocker cpto copy files out - Attach a debug container that shares the target's namespaces. With
--pid container:apiyou see its processes, and with--network container:apiyourlocalhostis the app's. You can even browse the app's filesystem through/proc/1/root. The tools come from the debug image and the target stays untouched docker debug, where your Docker Desktop version offers it, automates this and gives a shell with tools in any container or image- Use a debug variant of the base image, like distroless
:debugtags that include BusyBox, in a non-production build - In Kubernetes, the same idea is an ephemeral container via
kubectl debug
That's the trade-off of minimal images: production images stay small, and the tooling lives in a separate, temporary container.
# Join the target's PID and network namespaces from a tools container docker run -it --rm \ --pid container:api --network container:api \ alpine:3.20 sh # inside: ps, wget -qO- localhost:8080, ls /proc/1/root/appWhat interviewers listen for- No shell means
docker exec shfails - Start with logs, inspect, top and cp
- Share PID and network namespaces with a tools container
- Browse the filesystem via
/proc/1/root docker debugor:debugimage variants
Likely follow-up: Why does
/proc/1/rootonly work if the debug container shares the PID namespace?- Look from outside first:
56.Builds are fast on your laptop but slow in CI, where every run starts from scratch. How do you reuse the build cache?hard
Ephemeral CI runners start with an empty BuildKit cache, so every layer is rebuilt. The fix is to export the cache somewhere persistent with
--cache-toand import it on the next run with--cache-from:registry: stores the cache as a separate artifact in your registry, and works anywhereinline: embeds cache metadata in the pushed image; simple, but it doesn't scale well to multi-stage buildsgha: uses the GitHub Actions cache servicelocal,s3andazblob: a directory or object storage
mode=min, the default, caches only the layers that end up in the final image;mode=maxalso caches intermediate stages, which matters when the expensive work happens in a build stage.More tips: make the Dockerfile cache-friendly first, with manifests before source and a good
.dockerignore; use a per-branch cache ref that falls back to main's; and note that the defaultdockerdriver supports these backends only with the containerd image store, otherwise use adocker-containerbuilder. Cache mounts aren't exported, so they help less on fresh runners.docker buildx build \ --cache-from type=registry,ref=registry.example.com/api:buildcache \ --cache-to type=registry,ref=registry.example.com/api:buildcache,mode=max \ -t registry.example.com/api:$GIT_SHA --push .What interviewers listen for- CI runners start with an empty cache
- Export with
--cache-to, import with--cache-from - Registry, inline, gha, local and cloud backends
mode=maxalso caches intermediate stages- A cache-friendly Dockerfile still comes first
Likely follow-up: What are the trade-offs between
mode=minandmode=max?57.How do containers on different Docker hosts communicate with each other?hard
On one host, a bridge network is enough. Across hosts, Docker uses an overlay network: a virtual network that spans several Docker daemons.
- The hosts must be joined into a Swarm, the control plane that distributes network state. Overlay networks need Swarm mode even for standalone containers
- Traffic between hosts is encapsulated with VXLAN, by default on UDP port 4789. Nodes also communicate on port 7946 and Swarm management uses 2377, so those ports must be open
- Swarm services get built-in DNS, a virtual IP that load-balances across replicas, and an ingress routing mesh so a published port answers on every node
- Standalone containers can join only if the network was created with
--attachable --opt encryptedencrypts application traffic with IPsec, at a performance cost
Without Swarm, you'd route container subnets between hosts yourself, use macvlan or ipvlan, or publish ports on each host behind a load balancer. In practice, most teams now get multi-host networking from Kubernetes, where a CNI plugin plays this role.
What interviewers listen for- Overlay networks span multiple Docker hosts
- Requires Swarm mode
- VXLAN encapsulation, UDP 4789 by default
--attachablelets standalone containers join- Kubernetes uses CNI plugins instead
Likely follow-up: What is the Swarm routing mesh, and how does it differ from host-mode publishing?
No questions match that filter.
Prefer multiple choice? All 25 Docker MCQs with answers →