Home Interview Docker Interview Questions: Deep-Dive Answers for DevOps Roles

Docker Interview Questions: Deep-Dive Answers for DevOps Roles

In Plain English 🔥
Imagine you're shipping a birthday cake to a friend across the country. Instead of hoping the bakery at their end can recreate the exact recipe, you ship the entire kitchen — oven, ingredients, and instructions — sealed in a box. When they open it, the cake comes out perfectly every time. That sealed box is a Docker container. It bundles your app and everything it needs to run, so it works the same on your laptop, a test server, and production — no 'it worked on my machine' excuses.
⚡ Quick Answer
Imagine you're shipping a birthday cake to a friend across the country. Instead of hoping the bakery at their end can recreate the exact recipe, you ship the entire kitchen — oven, ingredients, and instructions — sealed in a box. When they open it, the cake comes out perfectly every time. That sealed box is a Docker container. It bundles your app and everything it needs to run, so it works the same on your laptop, a test server, and production — no 'it worked on my machine' excuses.

Docker changed the way the industry ships software. Before containers, deploying an app meant wrestling with OS differences, missing libraries, and environment drift between dev and prod. Teams lost days — sometimes weeks — chasing bugs that only appeared in production. Docker didn't just fix that problem; it restructured how engineers think about deployment entirely. That's why it now appears in almost every DevOps job description, and why interviewers probe it hard.

The real value Docker provides isn't just 'running things in containers.' It's reproducibility, isolation, and portability packaged together. A container is a lightweight, self-sufficient unit that carries its dependencies with it. Two containers on the same host are completely unaware of each other's libraries. That isolation means you can run Python 2 and Python 3 apps side-by-side without conflict — something that used to require separate VMs, each burning gigabytes of RAM.

By the end of this article you'll be able to answer the 12 most common Docker interview questions with confidence — not just reciting definitions, but explaining the reasoning behind design choices, calling out real trade-offs, and demonstrating the kind of hands-on instinct interviewers are actually looking for.

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.

OptimisedNodeApp.dockerfile · DOCKERFILE
1234567891011121314151617181920212223
# ─── STAGE 1: Base OS + system packages (rarely changes — cached aggressively) ───
FROM node:20-alpine AS base

# Set working directory inside the container
WORKDIR /app

# ─── STAGE 2: Install dependencies (changes only when package.json changes) ───
# Copy ONLY the dependency manifests first — not the full source
# If package.json hasn't changed, Docker reuses the cached npm install layer
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# ─── STAGE 3: Copy source code (changes on every commit — placed last) ───
# Because this layer is last, a source change only invalidates one layer
COPY src/ ./src/

# Expose the port the app listens on (documentation only — doesn't publish it)
EXPOSE 3000

# CMD is the default process. Use array syntax (exec form) — NOT shell form.
# Shell form wraps the process in /bin/sh -c, which means signals (SIGTERM)
# from 'docker stop' won't reach your app, causing a 10-second forced kill.
CMD ["node", "src/server.js"]
▶ Output
$ docker build -t myapp:1.0 .
[+] Building 14.2s (8/8) FINISHED
=> [base 1/1] FROM node:20-alpine 0.3s (cached)
=> [2/4] WORKDIR /app 0.0s (cached)
=> [3/4] COPY package.json package-lock.json ./ 0.1s
=> [4/4] RUN npm ci --omit=dev 11.8s
=> [5/5] COPY src/ ./src/ 0.1s

# Second build after changing only a source file:
$ docker build -t myapp:1.1 .
[+] Building 0.4s (8/8) FINISHED
=> [base 1/1] FROM node:20-alpine 0.0s (cached)
=> [3/4] COPY package.json ... 0.0s (cached)
=> [4/4] RUN npm ci 0.0s (CACHED) ← npm install skipped!
=> [5/5] COPY src/ ./src/ 0.1s ← only this layer rebuilt
⚠️
Interview Gold:When asked 'how do you optimise Docker build times?' don't just say 'use caching.' Say: 'I order Dockerfile instructions from least-frequently-changed to most-frequently-changed, and I copy dependency manifests separately before copying source code. This ensures npm install or pip install only reruns when dependencies actually change, not on every code change.' That answer signals real experience.

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 and on teams with different OS conventions.

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.

storage_patterns.sh · BASH
12345678910111213141516171819202122232425262728293031323334353637
#!/bin/bash

# ─── PATTERN 1: Named Volume (Production databases) ───────────────────────────
# Docker manages the volume location — portable, survives container removal
docker volume create postgres_data

docker run -d \
  --name postgres_prod \
  -e POSTGRES_PASSWORD=secret \
  -v postgres_data:/var/lib/postgresql/data \ # named volume → persistent
  postgres:16-alpine

# Volume persists even after the container is removed
docker rm -f postgres_prod
docker volume ls  # postgres_data is still here


# ─── PATTERN 2: Bind Mount (Local development hot-reload) ─────────────────────
# Mount your local source code into the container so edits take effect immediately
# $(pwd) expands to your current directory on the host
docker run -d \
  --name api_dev \
  -v $(pwd)/src:/app/src \ # host path → container path (tightly coupled to host)
  -p 3000:3000 \
  myapp:dev

# Editing ./src/server.js on your host instantly reflects inside the container


# ─── PATTERN 3: tmpfs Mount (Sensitive ephemeral data) ────────────────────────
# Data lives only in RAM — never touches disk — wiped when container stops
docker run -d \
  --name token_processor \
  --tmpfs /run/secrets:rw,noexec,nosuid,size=64m \ # 64MB RAM-backed mount
  myapp:prod

# If someone gets a disk image of your host, the secrets are never there
▶ Output
$ docker volume ls
DRIVER VOLUME NAME
local postgres_data

$ docker inspect postgres_data | grep Mountpoint
"Mountpoint": "/var/lib/docker/volumes/postgres_data/_data"

# After docker rm -f postgres_prod:
$ docker volume ls
DRIVER VOLUME NAME
local postgres_data ← still here, data is safe
⚠️
Watch Out:A bind mount of your source code into a container running as root means the container can modify your host files as root. In development that's usually fine. In any shared or CI environment, always run containers with a non-root user (USER appuser in your Dockerfile) and set appropriate directory permissions before going anywhere near production.

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. Only use this for latency-critical workloads where the overhead of bridge networking measurably matters.

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.

In Docker Compose, every service gets added to a default network named after your project. Services reference each other by service name — which is why your app config can say db_host: postgres and it just works.

docker-compose.networking.yml · YAML
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
version: '3.9'

services:
  # ─── API Service ───────────────────────────────────────────────────────────
  api:
    build: ./api
    ports:
      - "3000:3000"   # Publishes container port 3000 to host port 3000
    environment:
      # Uses the service name 'database' as the hostname — Docker DNS resolves it
      DATABASE_URL: postgres://appuser:secret@database:5432/myapp
      REDIS_URL: redis://cache:6379
    networks:
      - backend_net   # Connected to the private backend network
    depends_on:
      database:
        condition: service_healthy  # Won't start until postgres passes health check

  # ─── Database Service ───────────────────────────────────────────────────────
  database:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend_net   # Same network as api — they can talk by service name
    # No 'ports' key here — database is NOT exposed to the host, only to backend_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 5

  # ─── Cache Service ──────────────────────────────────────────────────────────
  cache:
    image: redis:7-alpine
    networks:
      - backend_net   # Same private network
    # Redis also has no published ports — can't be reached from outside the host

networks:
  backend_net:
    driver: bridge  # Default — creates isolated virtual network

volumes:
  postgres_data:
▶ Output
$ docker compose up -d
[+] Running 4/4
✔ Network myapp_backend_net Created
✔ Container myapp-cache-1 Started
✔ Container myapp-database-1 Started (health: starting)
✔ Container myapp-api-1 Started (waited for healthy database)

# From inside the api container, 'database' resolves to the postgres container IP:
$ docker exec myapp-api-1 nslookup database
Server: 127.0.0.11 ← Docker's built-in DNS resolver
Address: 127.0.0.11:53
Name: database
Address: 172.18.0.3 ← private IP of the database container

# The database port is NOT reachable from the host:
$ psql -h localhost -p 5432 -U appuser myapp
psql: error: connection refused ← correct — it's only on backend_net
🔥
Pro Tip:Never publish your database port to the host (ports: - '5432:5432') in any environment beyond your own local machine. Interviewers specifically look for candidates who understand that internal services should live on private networks and only edge services (APIs, reverse proxies) should have published ports. Mentioning this proactively signals security awareness.

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.

For JVM languages you typically go from a full JDK for compilation to a slim JRE for running. For Node.js you go from a full node image with devDependencies to a production install with only runtime dependencies.

Smaller images mean faster pulls from the registry, smaller attack surface (fewer binaries means fewer CVEs), faster CI pipelines, and lower egress costs when images are distributed across regions. These are the kinds of concrete trade-offs that turn an answer from textbook into impressive.

MultiStage.GoApp.dockerfile · DOCKERFILE
123456789101112131415161718192021222324252627282930313233343536373839404142434445
# ════════════════════════════════════════════════════════════════════════
# STAGE 1Builder
# Uses the full Go toolchain to compile the binary
# This stage is DISCARDED from the final image — it never ships to production
# ════════════════════════════════════════════════════════════════════════
FROM golang:1.22-alpine AS builder

# Install git only if your go modules reference private repos via git
RUN apk add --no-cache git

WORKDIR /build

# Download dependencies first (cached separately from source changes)
COPY go.mod go.sum ./
RUN go mod download

# Copy source and build
COPY . .

# CGO_ENABLED=0 produces a statically linked binary with no libc dependency
# GOOS=linux ensures the binary targets Linux even if you build on macOS
# -ldflags trims debug info and embedded module paths to reduce binary size
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-w -s" \
    -o /build/api_server \
    ./cmd/api

# ════════════════════════════════════════════════════════════════════════
# STAGE 2Final image
# Starts completely fresh — no Go toolchain, no source code, no build cache
# ════════════════════════════════════════════════════════════════════════
FROM scratch AS final

# Copy TLS certificates from builder so HTTPS calls inside the app work
# scratch has nothing — not even ca-certificates — so we must bring our own
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy ONLY the compiled binary from the builder stage
COPY --from=builder /build/api_server /api_server

# Document the port (doesn't publish it)
EXPOSE 8080

# Run the binary directly — no shell available in scratch, so exec form is required
CMD ["/api_server"]
▶ Output
$ docker build -t go-api:latest .
[+] Building 38.4s (12/12) FINISHED

$ docker images go-api
REPOSITORY TAG IMAGE ID SIZE
go-api latest a1b2c3d4e5f6 9.2MB ← entire production image is 9MB

# Compare to a naive single-stage build:
REPOSITORY TAG IMAGE ID SIZE
go-api-fat latest 9f8e7d6c5b4a 842MB ← includes full Go toolchain

# That's a 98.9% reduction in image size
# 9MB vs 842MB means roughly 90x faster cold pulls from the registry
⚠️
Interview Gold:If asked 'how do you reduce Docker image size?', give the three-layer answer: (1) use multi-stage builds to separate build-time from runtime dependencies, (2) choose a minimal base image like Alpine or distroless rather than ubuntu or debian, and (3) chain RUN commands with && and clean up package caches in the same layer (RUN apt-get install ... && rm -rf /var/lib/apt/lists/*) so the cache files never become a permanent layer. Each point shows a different dimension of understanding.
AspectDocker VolumeBind Mounttmpfs Mount
Managed byDocker daemonYou (host path)Docker daemon (RAM)
Data persists after container stopYesYes (it's a host file)No — gone immediately
Data persists after docker rmYesYes (it's a host file)N/A — already gone
Best use caseProduction databases, uploadsDev hot-reload, config injectionSecrets, session tokens, scratch space
PortabilityHigh — works on any Docker hostLow — tied to host path structureHigh — host-agnostic
PerformanceGood (slight overhead)Best (direct host I/O)Excellent (RAM speed)
Visible to 'docker volume ls'YesNoNo
Works in Docker ComposeYes — named volume syntaxYes — relative path syntaxYes — 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.

⚠ Common Mistakes to Avoid

  • Mistake 1: Running containers as root unnecessarily — Your app inside the container runs as root by default, meaning a vulnerability in the app gives an attacker root access inside the container. Fix it by adding USER instructions in your Dockerfile: create a non-root user with RUN addgroup -S appgroup && adduser -S appuser -G appgroup and switch to it with USER appuser before the CMD. Most application workloads need no root privileges at runtime whatsoever.
  • Mistake 2: Using shell form for CMD and ENTRYPOINT — Writing CMD npm start instead of CMD ["npm", "start"] wraps your process in /bin/sh -c, making sh the PID 1 process. When you run docker stop, Docker sends SIGTERM to PID 1 (sh), not to your app. sh doesn't forward signals, so your app gets a hard SIGKILL after the grace period (10 seconds by default), potentially corrupting in-flight requests. Always use exec form (array syntax) so your app is PID 1 and receives signals directly.
  • Mistake 3: Copying the entire build context including node_modules or .git — Running COPY . . without a .dockerignore file copies your entire local directory, including node_modules (potentially GBs), .git history, .env files with secrets, and local log files. This bloats the image, leaks sensitive data, and slows every build because Docker hashes the entire context. Fix: create a .dockerignore file at the project root listing node_modules, .git, .env, *.log, dist, and any other non-essential paths. It works exactly like .gitignore and is not optional in real projects.

Interview Questions on This Topic

  • QExplain the difference between CMD and ENTRYPOINT in a Dockerfile. When would you use both together?
  • QA container is writing logs to a file inside the container filesystem. After restarting the container, the logs are gone. How do you fix this, and what are the trade-offs between the storage options available to you?
  • 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.

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 containers differ from virtual machines in a Docker interview context?

VMs virtualise hardware — each VM runs a full OS kernel on top of a hypervisor, consuming gigabytes of RAM just for the OS. Containers share the host OS kernel and virtualise only at the process level using Linux namespaces and cgroups. A container starts in milliseconds and uses megabytes of memory; a VM takes minutes and uses gigabytes. The trade-off is isolation: VMs provide stronger security boundaries because each has its own kernel, while containers share the host kernel — a kernel vulnerability could potentially affect all containers on the host.

Why does Docker cache get invalidated unexpectedly during a build?

Docker invalidates a layer's cache when either the instruction itself changes or any file copied into that layer has changed (based on content hash). The most common culprit is COPY . . appearing before RUN npm install — any source file change invalidates the COPY layer, which cascades to invalidate the npm install layer and forces a full dependency reinstall. Fix this by copying only package.json and package-lock.json first, running npm install, then copying the rest of the source. Additionally, ADD instructions that fetch URLs are never cached because Docker can't detect if the remote content changed.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousTop DevOps Interview QuestionsNext →Kubernetes Interview Questions
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged