Docker Interview Questions: Deep-Dive Answers for DevOps Roles
- Instruction order in a Dockerfile is a performance decision — least-volatile instructions (OS packages, dependency installs) must come before most-volatile ones (source code copy) or you eliminate all caching benefit and rebuild from scratch on every code change.
- Named volumes survive 'docker rm'; bind mounts are host filesystem paths that survive by definition; tmpfs is RAM-only and is the only Docker storage mechanism that guarantees data never touches disk — critical for ephemeral secrets handling.
- Docker's built-in DNS lets containers on the same network resolve each other by service name, not IP address. Never hardcode container IPs — they change on every restart. Always reference services by name and let Docker handle the resolution.
- Images vs containers: immutable template vs running instance with writable layer
- Layer caching: instruction order determines build speed
- Volumes vs bind mounts vs tmpfs: three storage mechanisms with different lifecycle guarantees
- Networking: bridge/host/none drivers, DNS resolution by service name
- Multi-stage builds: separate build-time from runtime dependencies
Container crashed or restarting in a loop.
docker logs --tail 50 <container>docker inspect <container> --format='{{.State.ExitCode}} {{.State.OOMKilled}}'Container running but not responding to requests.
docker port <container>docker exec <container> ps auxTwo containers cannot communicate on the same network.
docker network inspect <network> | grep -A 5 Containersdocker exec <container> nslookup <target-service>docker build is extremely slow.
docker history <image> | head -20du -sh . (in build context directory)Secrets exposed in docker history or docker inspect.
docker history --no-trunc <image> | grep -i 'password\|secret\|key'docker inspect <image> | grep -i ENVProduction Incident
Production Debug GuideSystematic debugging paths that demonstrate production experience.
Docker interview questions probe production instincts, not memorized definitions. Interviewers want to know if you have debugged a container that could not reach its database, optimized a build that took 10 minutes, or tracked down a secret leak in an image layer.
The three pillars that separate strong answers from weak ones: understanding the image/container lifecycle (immutable templates vs ephemeral instances), mastering layer caching (instruction order as a performance decision), and knowing the storage and networking trade-offs (named volumes vs bind mounts, bridge vs host networking).
Common misconceptions that fail interviews: EXPOSE publishes a port (it does not), containers are VMs (they share the host kernel), and docker stop and docker kill are the same (SIGTERM vs SIGKILL). Getting these wrong signals a lack of hands-on production experience.
Core Concepts: Images, Containers, and the Daemon — What Interviewers Really Want to Hear
Most candidates can define an image and a container. What separates a strong answer is explaining the relationship between them.
An image is an immutable, layered snapshot of a filesystem and its metadata — think of it as a read-only template. A container is a running instance of that image, plus a thin writable layer on top. When the container dies, that writable layer is gone. This is why containers are considered ephemeral by design.
The Docker daemon (dockerd) is the long-running background process that does the actual work: building images, managing container lifecycles, handling networking, and talking to registries. The Docker CLI you type commands into is just a client that sends API requests to the daemon over a Unix socket.
Interviewers love asking about layers because they reveal whether you understand caching. Every instruction in a Dockerfile creates a new layer. Layers are cached by their content hash. If layer 3 changes, every layer after it is invalidated and must be rebuilt. This is why instruction order in a Dockerfile matters enormously for build speed — put the things that change least (installing OS packages) at the top, and the things that change most (copying your app source code) near the bottom.
Copy-on-Write (CoW) internals: Docker's storage drivers (overlay2 is the default) use Copy-on-Write. When a container reads a file, it reads directly from the image layer. When it writes, the file is copied to the writable layer and modified there. This means multiple containers sharing the same image share the same read-only layers in memory — only the writable deltas are unique per container. This is why starting 50 containers from the same image is fast and memory-efficient.
# ─── STAGE 1: Base OS + system packages ─── FROM node:20-alpine AS base # Namespace branding LABEL maintainer="engineering@thecodeforge.io" WORKDIR /app # ─── STAGE 2: Install dependencies (Layer Caching Strategy) ─── # Copy only manifests to leverage Docker layer cache COPY package.json package-lock.json ./ RUN npm ci --omit=dev # ─── STAGE 3: Application Source ─── COPY src/ ./src/ EXPOSE 3000 # Exec form ensures SIGTERM handling CMD ["node", "src/server.js"]
---> Using cache
Successfully built a1b2c3d4e5f6
Successfully tagged io.thecodeforge/node-app:latest
- Each layer's content hash depends on the layer below it. A changed instruction produces a different hash.
- Docker caches by hash. If the hash changes, the cache is invalidated for that layer and all layers above.
- This cascading invalidation is why instruction order matters — put stable instructions first.
- The layer cache is local to the build machine. CI runners without warm caches rebuild everything.
Volumes vs Bind Mounts vs tmpfs — The Storage Question That Trips People Up
Data persistence is one of Docker's most misunderstood areas, and interviewers use it to separate people who've read the docs from people who've debugged production.
Containers are ephemeral. The writable layer that gets created when a container starts is destroyed when the container is removed. If you write a database file into that layer, you lose it the moment the container exits. The three storage mechanisms Docker offers each solve this differently.
A named volume is managed entirely by Docker. Docker decides where on the host filesystem the data lives (usually /var/lib/docker/volumes/). Your container just sees a directory. Volumes survive container deletion, can be shared between containers, and work across platforms. Use volumes for anything you care about keeping — databases, uploads, generated certificates.
A bind mount maps a specific host path into the container. You control the path. This is powerful for local development — you mount your source code directory into the container and edits you make on the host appear instantly inside the container, enabling hot-reload workflows. But bind mounts are tightly coupled to host filesystem layout, which makes them fragile in production.
tmpfs mounts are stored in the host's memory only. The moment the container stops, the data is gone. Use tmpfs for sensitive temporary data you explicitly do not want written to disk — think secrets, session tokens, or scratch space for cryptographic operations.
Failure scenario — bind mount in production causes data loss: A team ran PostgreSQL in Docker Compose with a bind mount: -v /data/postgres:/var/lib/postgresql/data. During a server migration, they copied the container configuration but not the host directory. The new server started with an empty mount, PostgreSQL initialized a fresh database, and the team deleted the old server. All production data was lost. The fix: use named volumes (docker volume create) which are managed by Docker and can be backed up and migrated independently of the host filesystem.
Performance trade-off: Named volumes use Docker's storage driver (overlay2 by default) which adds a thin abstraction layer. Bind mounts go through the host filesystem directly, which can be faster for I/O-intensive workloads. In benchmarks, bind mounts outperform named volumes by 5-15% on write-heavy database workloads. But the portability and management benefits of named volumes far outweigh this performance difference for production use.
#!/bin/bash # Production Pattern: Create a managed volume for the database docker volume create thecodeforge_db_data docker run -d \ --name forge-db \ -v thecodeforge_db_data:/var/lib/postgresql/data \ postgres:16-alpine # Security Pattern: Using tmpfs for API keys in memory docker run -d \ --name forge-sec-processor \ --mount type=tmpfs,destination=/app/secrets,tmpfs-size=64m \ io.thecodeforge/processor:latest
DRIVER VOLUME NAME
local thecodeforge_db_data
- Bind mounts couple the container to a specific host path — breaks portability across machines.
- If the host directory does not exist, Docker creates it as root — permission issues on subsequent runs.
- Host filesystem permissions can conflict with container user permissions.
- Server migration requires manually copying the host directory — easy to forget, impossible to recover from.
- Named volumes are managed by Docker, portable, and can be backed up with docker volume commands.
Docker Networking Deep Dive — How Containers Actually Talk to Each Other
Networking is where many Docker users hit a wall. The mental model that unlocks it: each Docker network is a private virtual switch. Containers attached to the same switch can talk to each other by container name. Containers on different switches can't reach each other unless you explicitly connect them or use a shared network.
Docker ships with three built-in network drivers. The bridge driver (default) creates a private network on the host. Containers on the same bridge network can communicate with each other using DNS — Docker has a built-in DNS server that resolves container names and service names automatically. This is how a Node.js API container can connect to a Postgres container using the hostname 'postgres' rather than an IP address that changes every restart.
The host driver removes the network namespace entirely. The container uses the host's network stack directly. This gives maximum network performance (no virtual switch overhead) but destroys isolation — the container can see all host ports.
The none driver disables networking completely. The container has only a loopback interface. Useful for running batch jobs that must be air-gapped, or for testing how your app behaves with no network access.
Connection refused debugging: When two containers cannot communicate, the most common causes are: (1) they are on different networks — verify with docker network inspect, (2) the target service binds to 127.0.0.1 instead of 0.0.0.0 — localhost inside a container is the container itself, not the host, (3) the connection string uses the host-mapped port instead of the container port — container-to-container communication uses the container port directly, (4) a healthcheck or depends_on race condition — the target service is not ready yet.
Performance trade-off — bridge vs host networking: Bridge networking adds a virtual Ethernet pair and iptables NAT rules for each container. This adds approximately 10-50 microseconds of latency per packet compared to host networking. For latency-sensitive workloads (high-frequency trading, real-time gaming), host networking eliminates this overhead. But it destroys port isolation — two containers cannot bind to the same port on host networking.
version: '3.9' services: api: image: io.thecodeforge/api:latest networks: - forge_internal environment: - DB_HOST=database database: image: postgres:16-alpine networks: - forge_internal healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 networks: forge_internal: driver: bridge
[+] Running 3/3
✔ Network forge_internal Created
✔ Container database Started
✔ Container api Started
- Docker's embedded DNS server at 127.0.0.11 resolves container names to internal IPs.
- Each container's /etc/resolv.conf points to this embedded DNS server automatically.
- The DNS server is network-aware — it only resolves names for containers on the same network.
- This is why containers on different networks cannot resolve each other — the DNS server enforces network isolation.
Multi-Stage Builds and Image Size — The Optimisation Question That Defines Seniors
A production Docker image should contain only what is needed to run the application. Not the compiler. Not the test framework. Not the build tools. Most candidates understand single-stage builds. Seniors reach for multi-stage builds by default.
The idea is simple: use one stage to build your app (with all the heavyweight tools that requires), then start fresh from a minimal base image and copy only the compiled output. The final image has no knowledge of how it was built — just what needs to run.
For a Go application this is dramatic: the builder stage might pull in the entire Go toolchain (hundreds of MB), but the final stage starts from scratch (literally 'FROM scratch') and contains only the statically compiled binary — often under 10MB total image size.
Security benefit: Every tool in your production image is an attack surface. gcc, make, curl, wget — if an attacker gets shell access to your container, these tools let them compile exploits, download payloads, and pivot. A distroless or Alpine runtime image with no build tools gives an attacker almost nothing to work with.
Deployment speed impact: Container images must be pulled to every node before they can run. A 1.2GB image takes 30-60 seconds to pull over a fast network. A 12MB image pulls in under 1 second. During rolling deployments across 20 nodes, that difference is minutes of deployment time.
Why deleting files in a RUN command does not shrink the image: Docker layers are additive. If you RUN apt-get install gcc in one layer and RUN apt-get remove gcc in the next, the gcc files still exist in the first layer — the image size does not decrease. The second layer just marks them as deleted. Multi-stage builds solve this by starting a fresh stage — the runtime stage never contains build tools in any layer.
# ─── STAGE 1: Build Environment ─── FROM golang:1.22-alpine AS builder WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -o /app/forge-binary main.go # ─── STAGE 2: Production Distroless ─── # Using distroless for security and minimal footprint FROM gcr.io/distroless/static-debian12 COPY --from=builder /app/forge-binary /forge-binary USER nonroot:nonroot ENTRYPOINT ["/forge-binary"]
io.thecodeforge/app latest d8f3e2a1b0c9 12.4MB
- Docker layers are additive. A file added then deleted in a later layer still occupies space in the earlier layer.
- RUN apt-get install gcc && apt-get remove gcc still has gcc in the install layer.
- Multi-stage builds start fresh — the runtime stage never contains build tools in any layer.
- This is the only way to genuinely reduce image size, not just hide files from the filesystem.
| Aspect | Docker Volume | Bind Mount | tmpfs Mount |
|---|---|---|---|
| Managed by | Docker daemon | You (host path) | Docker daemon (RAM) |
| Data persists after container stop | Yes | Yes (it's a host file) | No — gone immediately |
| Data persists after docker rm | Yes | Yes (it's a host file) | N/A — already gone |
| Best use case | Production databases, uploads | Dev hot-reload, config injection | Secrets, session tokens, scratch space |
| Portability | High — works on any Docker host | Low — tied to host path structure | High — host-agnostic |
| Performance | Good (slight overhead) | Best (direct host I/O) | Excellent (RAM speed) |
| Visible to 'docker volume ls' | Yes | No | No |
| Works in Docker Compose | Yes — named volume syntax | Yes — relative path syntax | Yes — tmpfs key |
🎯 Key Takeaways
- Instruction order in a Dockerfile is a performance decision — least-volatile instructions (OS packages, dependency installs) must come before most-volatile ones (source code copy) or you eliminate all caching benefit and rebuild from scratch on every code change.
- Named volumes survive 'docker rm'; bind mounts are host filesystem paths that survive by definition; tmpfs is RAM-only and is the only Docker storage mechanism that guarantees data never touches disk — critical for ephemeral secrets handling.
- Docker's built-in DNS lets containers on the same network resolve each other by service name, not IP address. Never hardcode container IPs — they change on every restart. Always reference services by name and let Docker handle the resolution.
- Multi-stage builds are not an optimisation you do later — they're the default pattern for any compiled language. Shipping a Go or Java app in a single-stage image that includes the compiler is equivalent to shipping a kitchen with every meal you deliver.
- Shell form CMD wraps your process in /bin/sh which does not forward SIGTERM. Your app gets SIGKILL after 10 seconds. Always use exec form (array syntax) for production containers.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QExplain the difference between CMD and ENTRYPOINT in a Dockerfile. How do they interact when used together in a production-grade container?
- QLeetCode Standard: Given a high-traffic microservice architecture, how would you troubleshoot a 'Connection Refused' error between two containers on the same custom bridge network?
- QYour CI pipeline is pulling a 1.2GB Docker image on every run and build times are unacceptable. Walk me through every optimisation you'd apply — to the image itself, the Dockerfile, and the registry setup.
- QExplain the Copy-on-Write (CoW) strategy used by Docker's storage drivers and how it impacts container performance.
- QWhat is the difference between docker stop and docker kill? Why does this matter for zero-downtime deployments?
- QA container is running but you cannot reach it from the host on the mapped port. Walk me through your debugging process.
- QYou have a Dockerfile that installs gcc, compiles a C extension, then runs apt-get remove gcc. The image is still 800MB. Why, and how do you fix it?
- QHow would you handle secrets (API keys, database passwords) in a Dockerized application across local development, CI, and production?
Frequently Asked Questions
What is the difference between a Docker image and a Docker container?
An image is a read-only, layered template — think of it as a frozen snapshot of a filesystem and its metadata. A container is a live, running instance of that image with a thin writable layer added on top. You can create dozens of containers from a single image simultaneously, each isolated from the others. When a container is deleted, its writable layer is gone, but the original image is untouched.
How do you handle secrets like passwords or API keys in Docker containers?
Secrets should never be baked into Docker images. For local dev, use environment variables or .env files (in .gitignore). In production, use orchestration-level features like Docker Secrets or Kubernetes Secrets. Within a standalone container, mounting a tmpfs volume is a secure way to pass sensitive data into memory so it never persists to the host disk. BuildKit --mount=type=secret handles build-time secrets without leaving them in image layers.
Can you run a container without the Docker Daemon?
Yes. While the Docker engine relies on the daemon, the actual container execution is handled by lower-level runtimes like containerd or runc. Tools like Podman provide a daemonless alternative that implements the same OCI standards as Docker, allowing you to run containers as a standard user process without a background service.
What does exit code 137 mean for a Docker container?
Exit code 137 means the container was killed by signal 9 (SIGKILL), typically by the Linux OOM killer. The container exceeded its memory limit or the host ran out of memory. Debug with docker inspect to check OOMKilled status, then either increase the memory limit (--memory flag) or fix the memory leak in your application.
What is the difference between docker stop and docker kill?
docker stop sends SIGTERM (graceful shutdown) and waits 10 seconds (configurable with --stop-timeout), then sends SIGKILL if the process has not exited. docker kill sends SIGKILL immediately with no grace period. In production, always use docker stop to give your application time to flush logs, close database connections, and drain in-flight requests. docker kill is for stuck containers that do not respond to SIGTERM.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.