Master your next Docker interview with our comprehensive collection of questions and expert-crafted answers. Get prepared with real scenarios that top companies ask.
Prepare for your Docker interview with proven strategies, practice questions, and personalized feedback from industry experts who've been in your shoes.
Thousands of mentors available
Flexible program structures
Free trial
Personal chats
1-on-1 calls
97% satisfaction rate
Choose your preferred way to study these interview questions
Multi-stage builds let you use multiple FROM statements in one Dockerfile, where earlier stages build or prepare artifacts and the final stage contains only what you need to run. You can COPY --from=builder into a clean runtime image, so build tools never ship to production.
alpine or distroless.Docker volumes are managed by Docker, while bind mounts map a specific host path into the container. That difference matters for portability, safety, and day to day workflow.
/app.I’d frame Docker as OS-level virtualization, while VMs are hardware-level virtualization. A VM bundles a full guest OS, app, and dependencies, so it is heavier and slower to boot. A container shares the host OS kernel and only packages the app plus its libraries and runtime, so it is much lighter and starts fast.
If they know VMs well, I’d say Docker is usually for packaging and deploying apps, while VMs are better when you need a full separate OS.
Try your first call for free with every mentor you're meeting. Cancel anytime, no questions asked.
Containerization solves the classic, "it works on my machine" problem. It packages an app with its runtime, libraries, and config so it runs the same across laptops, test environments, and production. Compared to virtual machines, containers are much lighter because they share the host OS kernel, so they start faster, use fewer resources, and make scaling easier.
Docker took off because it made containers practical and developer-friendly.
- Simple workflow, Dockerfile, docker build, docker run
- Portable images that run consistently across environments
- Huge ecosystem, Docker Hub, Compose, tooling, community support
- Strong fit for CI/CD, microservices, and cloud-native deployments
- It standardized how teams build, ship, and run software
So the big value is consistency, speed, and operational simplicity.
A Docker image is the blueprint, a Docker container is the running instance of that blueprint.
docker build and stored in registries like Docker Hub.docker run, and they can be started, stopped, deleted, and recreated easily.A simple analogy is class vs object, or recipe vs meal. The image defines what to run, the container is that thing actually running.
I usually answer this in layers: shrink the base, shrink what you copy, and shrink what you ship.
alpine, distroless, or a slim runtime image, if your app supports it..dockerignore to exclude node_modules, .git, logs, test data, and local build artifacts.COPY . . when possible.npm ci --only=production or equivalent.When you run a container, Docker takes a read-only image and adds a thin writable layer on top. The image provides the filesystem, app, libraries, and metadata, while the container is the live runtime instance of that image.
ENTRYPOINT or CMD as PID 1 inside that isolated environment.So the key idea is, images are templates, containers are running processes created from those templates.
A Dockerfile is a text file of build instructions that creates a container image, basically your app’s recipe. It defines the base image, dependencies, app files, runtime settings, and startup command.
python:3.12-slim or node:alpine when appropriate.package.json or requirements.txt, then run install, then COPY . . for better cache reuse..dockerignore to avoid copying node_modules, .git, logs, and secrets.WORKDIR, use explicit CMD or ENTRYPOINT, and pin versions for reproducibility.Get personalized mentor recommendations based on your goals and experience level
Start matchingThey serve different roles in the image lifecycle.
RUN executes during docker build, and creates a new image layer. Use it to install packages, copy artifacts into place, or compile code.CMD sets the default command or arguments when a container starts. It is easy to override with docker run ....ENTRYPOINT defines the main executable for the container. It makes the container behave like a specific app or command.ENTRYPOINT when the container should always run one thing, like nginx or your app binary.CMD to provide default arguments to ENTRYPOINT, or as a fallback command if no command is supplied.Common pattern: ENTRYPOINT ["python","app.py"] with CMD ["--port","8080"]. Then users can override just the args.
.dockerignore keeps unnecessary files out of the build context before Docker sends it to the daemon. That makes builds faster, images smaller, and reduces the chance of accidentally copying secrets or local junk into the image.
Typical exclusions:
- Git metadata, like .git, .gitignore, .github
- Local dependencies and build output, like node_modules, dist, build, target
- Editor and OS files, like .vscode, .idea, .DS_Store
- Logs and temp files, like *.log, tmp, .cache
- Secrets and env files, like .env, *.pem, private keys
- Test artifacts or docs not needed at runtime, depending on the image
One nuance, .dockerignore affects the build context, not just COPY, so excluding big folders can dramatically speed up builds.
Docker uses OS-level virtualization, while hypervisors virtualize hardware. That is the core difference. With Docker, containers share the host kernel, so they are lightweight and start fast. With a hypervisor, each VM includes a full guest OS, which adds more overhead.
Think of it as client, server, and the full runtime package.
docker build, docker run, or docker ps.dockerd, that actually builds images, creates containers, manages networks, volumes, and talks to the OS.The CLI sends requests to the daemon through the Docker API, over a local socket or TCP. So when you type docker run nginx, the CLI is just the front end, the daemon does the real work, and Docker Engine is the complete system that provides that capability.
Docker images are built as a stack of immutable layers. Each Dockerfile instruction like RUN, COPY, or ADD usually creates a new layer, and containers add a thin writable layer on top at runtime. When you change one instruction, Docker only rebuilds that layer and the ones after it, not everything before it.
That is why layers help both developer speed and storage efficiency.
I start by treating it as either an app problem, an entrypoint problem, or a missing dependency/config problem.
docker ps -a, docker logs <container>, and docker inspect <container> for exit code, error text, and command.CMD or ENTRYPOINT is correct and not running a short-lived script.docker run -it --entrypoint sh <image> or bash to inspect files, env vars, permissions, and startup commands.docker inspect, host dmesg, and container restart policy.I’d debug it from the bottom up, network, DNS, ports, then app binding.
docker inspect <container> and check Networks.docker exec -it <c1> getent hosts <c2> or ping <c2> if available.0.0.0.0, not just 127.0.0.1.docker exec -it <c1> curl http://<c2>:<port> or use nc -zv <c2> <port>.docker network inspect <network> and look for aliases, subnet issues, or disconnected endpoints.If it still fails, I’d recreate both on a clean user-defined bridge network, because default bridge behavior and stale config can cause surprises.
They do different things in the container lifecycle.
docker stop asks the main process to exit cleanly, it sends SIGTERM, then after a grace period sends SIGKILL if needed.docker kill forcefully stops the container immediately, by default with SIGKILL, though you can send another signal with -s.docker rm does not stop a running container unless you use -f, it removes the container metadata and writable layer from Docker.stop for normal shutdowns, so apps can finish requests and flush data.kill when a container is hung or ignores stop.rm when the container is already stopped and you want to delete it.I choose a base image by balancing security, size, compatibility, and maintainability.
node, python, nginx, or just a minimal OS if I need full control.alpine or distroless, but only if the app and dependencies are compatible.debian-slim is easier than ultra-minimal images.In practice, I usually develop with something like debian-slim, then optimize later if startup time, size, or attack surface matters.
It comes down to size versus compatibility and operability.
musl instead of glibc, which can break binaries, native dependencies, or cause odd runtime behavior.My rule is, optimize for reliability first, then size. If Alpine saves 100 MB but costs hours in debugging, it is rarely worth it.
I separate build-time settings from runtime config, and I avoid baking secrets into images. The image should be portable, while each environment injects what it needs at startup.
ENV in the Dockerfile only for safe defaults, not passwords or API keys.docker run -e, --env-file, or Compose environment and env_file.ARG and ENV solve different problems in Docker.
ARG is for build time only, used inside the Dockerfile while the image is being built.ENV is for runtime, it becomes part of the image and is available when the container starts.ARG values are not automatically present in the running container.ENV values can be overridden at docker run time with -e.ARG is commonly used for things like base image versions or conditional build steps.ENV is used for app config like ports, mode, API endpoints, or feature flags.Example: use ARG NODE_VERSION=20 to choose the Node image during build, and ENV NODE_ENV=production so the app sees that value when the container runs.
Docker networking gives each container its own network namespace, interfaces, routes, and DNS behavior. Docker creates virtual networks and connects containers with veth pairs, bridges, or overlays, depending on the driver. Containers on the same user-defined network can usually reach each other by name, and publishing a port maps container traffic to the host.
bridge, most common on a single host, good for app-to-app communicationhost, container shares the host network stack, best for performance, less isolationnone, disables networking, useful for tight isolation or custom setupsoverlay, multi-host networking in Docker Swarm, lets services talk across nodesmacvlan, gives containers their own MAC and LAN presence, useful for legacy appsI’ve used bridge the most, host for monitoring agents, and overlay in Swarm-based service deployments.
I’ve used Docker as the consistency layer across CI, testing, and releases, so the same artifact moves from build to deploy with minimal drift.
EXPOSE and --publish solve different problems.
EXPOSE is metadata in the image, usually set in the Dockerfile, like EXPOSE 8080.-p 8080:8080, creates a host-to-container mapping so traffic from the host can reach the app.-P can publish all exposed ports automatically to random host ports.So, think of EXPOSE as a hint or documentation, and publish as the actual network wiring.
In Docker Compose, services usually talk over a default bridged network that Compose creates for the project. Every service joins that network automatically, and Docker provides built-in DNS, so containers can reach each other by service name.
web and db, web can connect to db using host db, not localhost.db:5432 for Postgres.ports is for exposing to your host machine, not for container-to-container traffic.docker-compose.yml if you want isolation between groups of services.Typical example, an app container connects to MySQL with mysql://user:pass@db:3306/app.
Docker Compose is a tool for defining and running multi-container apps with one YAML file, usually docker-compose.yml or compose.yaml. Instead of manually starting each container with separate docker run commands, you declare services, networks, volumes, ports, and environment variables in one place, then use docker compose up.
It’s more useful when:
- Your app has multiple services, like a web app, database, Redis, and worker.
- You want repeatable local dev environments across teammates.
- You need service-to-service networking without wiring everything by hand.
- You want to manage shared config, volumes, and startup settings centrally.
- You need quick lifecycle commands, like up, down, logs, and restart.
For a single simple container, plain docker run is often enough.
I treat secrets as runtime-only data, never something baked into the image or committed to source control.
/run/secrets, not exposed as env vars by default..env files carefully, keep them out of Git with .gitignore, and separate real secrets from sample config.What I avoid:
- Hardcoding secrets in Dockerfile, app code, or docker-compose.yml
- Passing secrets as ENV in images, they can leak via image history or inspection
- Committing .env files or copying secret files into the image
- Using build args for sensitive values unless combined with secure build-time secret features like BuildKit secrets
In Compose, I treat startup ordering and readiness as two different problems. depends_on controls start order, but by itself it does not guarantee the dependency is actually ready to accept connections.
depends_on for basic sequencing, like app after db.healthcheck to the dependency, then use depends_on with condition: service_healthy when supported.wait-for-it or app-level retry logic.In practice, I prefer healthchecks plus retry logic in the app. That is more reliable than startup ordering alone, especially when containers restart or dependencies come up slowly.
A Docker build usually fails because of a few repeat offenders, and I investigate from the bottom of the error output upward, since the real failure is often near the end.
Dockerfile syntax, wrong instruction order, or invalid paths in COPY and ADD..dockerignore excluding something important.apt, apk, or npm.USER.I usually rebuild with docker build --progress=plain ., inspect the exact failing layer, add temporary RUN ls or cat checks, and verify the context and image tags first.
Using latest in production is risky because it is not actually a version, it is just a moving pointer. The same deployment config can pull different image contents over time, which kills predictability.
latest does not tell you what code is running.latest is overwritten.latest.In production, I would use immutable tags like 1.4.2 or, even better, pin by digest such as @sha256:... for exact traceability.
A Docker registry is a service that stores and distributes container images. It is where you push images after building them and where Docker pulls images from during deploys. Docker Hub is the most common example, but teams also use registries like Amazon ECR, GitHub Container Registry, or a self-hosted registry.
The secure pattern is to use short-lived credentials, never hardcode secrets, and let the CI system inject them at runtime.
echo $TOKEN | docker login REGISTRY -u USER --password-stdin, so it avoids shell history and logs.In practice, I usually wire CI to cloud IAM, mint a short token per job, log in, push, then let it expire automatically.
For PostgreSQL in Docker, I’d persist data with a Docker volume, not the container filesystem, because containers are disposable but volumes survive restarts and recreations.
/var/lib/postgresql/datadocker run -v pgdata:/var/lib/postgresql/data postgresvolumes: entry and attach it to the Postgres serviceIf this is Kubernetes, same concept, but with PersistentVolumes and PersistentVolumeClaims.
By default, data inside the container’s writable layer is deleted with the container. So if your app writes to /tmp, /var/lib/app, or anywhere not backed by a volume, that data is gone when you run docker rm.
The exception is persistent storage:
- Named volumes persist after container deletion, unless you remove them explicitly, like docker rm -v or docker volume rm.
- Bind mounts persist because the data lives on the host filesystem, not in the container.
- Anonymous volumes also persist by default, but they’re easier to orphan and forget.
- Image layers are unaffected, deleting a container does not delete the image.
- Best practice is to store important data in volumes or bind mounts, not the container layer.
For a running container, I usually think in three buckets: logs, metadata, and live resource stats.
docker logs <container>; add -f to follow, --tail 100 for recent lines, and -t for timestamps.docker inspect <container> returns JSON with env vars, mounts, network settings, IP, restart policy, and state.docker ps shows container ID, image, uptime, ports, and names.docker stats <container> shows live CPU, memory, network I/O, and block I/O; omit the name to see all containers.docker exec -it <container> sh or bash lets you inspect app logs, processes, and files directly.If I need one specific field from inspect, I use Go templates, like docker inspect -f '{{.State.Status}}' <container>.
Containers are lighter than VMs, so the biggest risk is shared kernel exposure. If a container breaks isolation, it can impact the host or other containers.
--cap-drop, no-new-privileges, avoid --privileged./var/run/docker.sock or host paths, mitigate by avoiding socket mounts, using read-only filesystems and limited volumes.Running containers as root is risky because root inside the container can still be powerful on the host, especially if there is a kernel bug, a bad volume mount, or someone adds extra capabilities. It increases blast radius. If that container is compromised, an attacker has a much better starting point.
In practice, I avoid it by:
- Setting a non-root user in the image with USER appuser after creating that user.
- Making sure app files are owned correctly with chown, so the process can read and write what it needs.
- Using a high port like 8080 instead of 80, since low ports often need root.
- Passing a UID:GID at runtime with docker run --user 1001:1001 when needed.
- In Kubernetes, setting runAsNonRoot: true and a specific runAsUser, plus dropping unnecessary Linux capabilities.
Health checks tell Docker whether the app inside the container is actually usable, not just whether the container process exists. A container can be running while the app is stuck, still starting, deadlocked, or returning errors.
running only means the main process has not exited.curl or pg_isready on a schedule.starting, healthy, or unhealthy based on the result./health returns 500, the container is running but unhealthy.So the key difference is process liveness versus application readiness and responsiveness.
I treat restart policies as a way to match container behavior to failure mode and operational intent. The key is deciding whether the app should self-heal, stay stopped for investigation, or only restart with the Docker daemon.
no: default, no automatic restart. Use for one-off jobs, debugging, or containers you want to inspect after failure.on-failure[:max-retries]: restarts only if the process exits non-zero. Best for batch jobs or workers that may fail transiently.always: restarts on any stop, including daemon restart. Good for long-running services that should always come back.unless-stopped: like always, but if you manually stop it, Docker keeps it stopped after daemon reboot. My usual choice for stable services.In practice, I use unless-stopped for web apps and APIs, on-failure for workers, and no for admin tasks.
Safest approach is to prune only unused resources and verify what is attached first. Docker will not remove anything actively used by running containers, but I still check before deleting.
docker ps -a, docker image ls, docker volume ls, docker network lsdocker system df, it shows reclaimable spacedocker container prunedocker image prune -a, only deletes images not used by any containerdocker network prunedocker volume prune, volumes can hold important data from stopped containersdocker system prune or docker system prune -a, add --volumes only if you are sureIn production, I usually label critical resources and avoid broad prune commands during deployments.
Tagging strategy has a huge impact on both safe rollouts and debugging. The short version is, human-friendly tags help operations, immutable identifiers help reliability.
latest or prod are convenient, but risky, because they can point to different images over time.app:1.4.2 and app:git-abc123, so teams get both readability and precision.latest, and keep labels or metadata linking the image to commit, build, and release info.I focus on controlling every input to the build, so the same source produces the same image every time.
package-lock.json, poetry.lock, go.sum, and build from those, not floating ranges..dockerignore, and copy only files needed for each layer.latest, apt-get upgrade, or commands that pull whatever is current that day.ARGs, locale, timezone, and explicit WORKDIR.apt-get update with install in one layer, pin versions, and clean caches consistently.I’d answer this in two parts, prevention and response.
alpine or distroless, and pin versions for repeatability.When critical issues show up, I first verify whether the package is actually reachable and exploitable. Then I patch by updating the base image or dependency, rebuild, retest, and redeploy. If no fix exists, I mitigate fast, reduce privileges, disable the vulnerable component, tighten network access, and document a time-bound exception.
I’d answer this with a quick STAR structure, situation, actions, result, then keep the example concrete and measurable.
On one team, our CI Docker build for a Node.js service had crept up to about 12 minutes, which was slowing every pull request. I profiled the Dockerfile and saw two main issues: we were copying the whole repo before installing dependencies, which busted cache constantly, and we were shipping build tools into the final image. I changed the order to copy only package.json and lockfiles first, ran dependency install there, then copied app code. I also added a multi-stage build so compilation happened in a builder image, while production used a slimmer runtime image. That cut build time to around 4 minutes, reduced image size by roughly 60 percent, and made CI much more predictable.
I’d answer this with a quick STAR format: situation, difference observed, how I narrowed it down, then the fix and prevention.
At a previous team, a Node.js app worked fine in Docker Compose locally, but in production on Kubernetes it kept failing health checks and restarting. I compared the image, env vars, startup command, mounted files, and resource limits between dev and prod. The key difference was that production had a stricter memory limit, and the app was also expecting a config file path that only existed in dev via a bind mount. I used kubectl logs, kubectl describe pod, and ran an interactive shell in the container to inspect the filesystem and env. We fixed it by baking the config into the image properly, externalizing env-specific settings, and setting realistic memory requests and limits. After that, we added parity checks in CI.
I’d answer this with a quick STAR structure, situation, action, resistance, result, then keep it concrete.
At one company, we had a legacy app that only ran reliably on a couple of senior engineers’ laptops. Onboarding took days, and CI failures were often environment-related. I introduced Docker first as a development consistency tool, not a platform overhaul, which helped lower the temperature.
Dockerfile and docker-compose setup, and kept the old workflow available.The key was making adoption incremental and showing immediate value.
I’d answer this with a quick STAR format: situation, impact, root cause, fix, and what changed afterward.
In one production incident, a Dockerized API started flapping across multiple hosts right after a deploy. Containers were restarting every few minutes, which caused intermittent 502s behind the load balancer. The root cause was a mismatch between the app’s startup time and the container HEALTHCHECK, plus an aggressive restart policy. The app needed warm-up time for cache hydration, but the health check started failing too early, so Docker kept marking it unhealthy and restarting it. I resolved it by increasing the health check start-period, tuning timeouts, and temporarily rolling back the image. After that, we added startup probes in our orchestrator, tested health checks in staging under production-like load, and documented sane defaults for container lifecycle settings.
I’d start with the network path from outside the host to the app inside the container, because that’s where most “works locally, unreachable on server” issues live.
0.0.0.0, not just 127.0.0.1 inside the container.docker ps, for example 0.0.0.0:80->8080/tcp.curl localhost:published_port, then from another machine.docker inspect for the actual container IP, ports, and health.If I had to prioritize, I’d check bind address, port mapping, and firewall in that order.
Yes. I’d answer this with a quick STAR structure: situation, key constraints, actions, and measurable outcome.
At a previous company, I helped containerize a legacy Java monolith that had grown around app server configs, local file storage, and a lot of environment-specific assumptions. The hardest parts were externalizing state, untangling startup dependencies, and making the image portable across dev, CI, and production. We moved config to env vars and secrets, pushed file storage to object storage, and used a multi-stage Docker build to keep the image small. Another challenge was slow builds and flaky startup health, so we added layer caching, health checks, and a proper entrypoint. Result was much more consistent deployments, faster onboarding, and fewer “works on my machine” issues.
I’d treat that as an environment mismatch until proven otherwise. Docker is perfect for turning “my machine” into something reproducible, then comparing it to failing environments.
Dockerfile that pins the base image, runtime, OS packages, and app dependencies.docker compose.docker inspect, docker exec, logs, and dependency versions between the working container and the failing target.I’d say, “Let’s package your exact setup in Docker, then we can prove whether the issue is code, config, or environment.”
I’d focus on cutting rebuild work, improving cache reuse, and reducing registry/network overhead in CI.
.dockerignore to shrink build context, especially excluding node_modules, git, test artifacts, and logs.buildx, then use remote cache with --cache-to and --cache-from so runners share layers.I’d treat it as both a security incident and a process gap. Technically, the first step is containment and rotation, because if secrets were baked into images or committed in Dockerfiles, you should assume they’re exposed.
Dockerfiles, build args, and layers; use runtime injection via Docker/Kubernetes secrets, Vault, or cloud secret managers.RUN --mount=type=secret so secrets never end up in layers.I would not force Docker everywhere. It is great for portability and consistency, but there are cases where the cost outweighs the benefit.
I would frame it as, use Docker when isolation, reproducibility, and deployment speed matter more than added complexity.
Knowing the questions is just the start. Work with experienced professionals who can help you perfect your answers, improve your presentation, and boost your confidence.
Comprehensive support to help you succeed at every stage of your interview journey
We've already delivered 1-on-1 mentorship to thousands of students, professionals, managers and executives. Even better, they've left an average rating of 4.9 out of 5 for our mentors.
Find Docker Interview Coaches