Ch. 12

Docker interview questions & answers

Containers explained: images and layers, writing efficient Dockerfiles, multi-stage builds, volumes, networking, Compose and security.

57 interview questions25 quiz questions0 notes
your progress0%

Top 57 Docker interview questions most asked first

  1. 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 docker CLI), 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. 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. 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 commit do, and why is it discouraged?

  4. 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:

    • FROM picks the base image, here Node 22 on slim Debian
    • WORKDIR sets, and creates if needed, the directory later instructions and the container use
    • The first COPY brings in only the dependency manifests, so the npm ci layer stays cached until dependencies change
    • RUN executes a command at build time and saves the result as a layer
    • The second COPY adds the source code
    • ENV sets an environment variable for later steps and for the running container
    • EXPOSE documents the port the app listens on; it doesn't publish it
    • USER switches to the non-root node user the official image provides
    • CMD is the default command at run time, in exec form so node runs as PID 1 and receives signals

    Build it with docker build -t my-api . and run it with docker 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; -p actually publishes
    • Run as non-root with USER

    Likely follow-up: Why use npm ci rather than npm install in a Dockerfile?

  5. 5.What is the difference between CMD and ENTRYPOINT? What does docker run my-image there print for this Dockerfile?mid

    ENTRYPOINT defines the executable the container always runs; CMD supplies default arguments, or the default command when there's no entrypoint. In exec form, Docker runs ENTRYPOINT followed by CMD.

    • docker run my-image prints hello world
    • docker run my-image there prints hello there: arguments after the image name replace CMD, not the entrypoint
    • docker run --entrypoint date my-image replaces the entrypoint and also clears the image's default CMD

    Shell form is different: ENTRYPOINT echo hello is wrapped in /bin/sh -c and ignores both CMD and any run arguments, so it would print just hello.

    A common pattern is ENTRYPOINT ["/docker-entrypoint.sh"] with CMD ["postgres"]: the script does setup, then runs exec "$@" so the real process replaces the shell. Use CMD alone when users should freely swap the command, and ENTRYPOINT when 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 run arguments replace CMD only
    • --entrypoint overrides 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. 6.What is the difference between COPY and ADD in a Dockerfile, and which should you use?easy

    Both copy files into the image, but ADD has 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 --unpack to control this
    • Current Dockerfile versions also accept a Git repository URL, and --checksum to verify a remote source

    COPY simply copies files from the build context, or with --from, from another build stage or image. Docker's guidance is to prefer COPY because it's explicit and predictable; the auto-extraction in ADD can surprise whoever reads the Dockerfile later. Reach for ADD only when you actually want one of its features, typically unpacking a local tarball.

    Both support --chown and --chmod, which set ownership and permissions as the files are copied. That's better than a later RUN 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 -R after a COPY increase image size?

  7. 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, COPY and ADD, each produce a layer holding just the files they added, changed or deleted. Others, like ENV or CMD, 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 COPY and ADD, the checksums of the source files are part of the key, so editing a copied file invalidates it
    • RUN is matched on its command string, not its output: RUN apt-get update won'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-cache or --pull when 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 update on its own line an anti-pattern?

  8. 8.What is a multi-stage build, and why would you use one?mid

    A multi-stage build has several FROM instructions in one Dockerfile. Each starts a new stage, and you COPY --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 a scratch or 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 --from takes only the needed artifacts
    • Build tools and dev dependencies never ship
    • --target builds 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. 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/volumes on 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=bind fails 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 /scratch
    What 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?

  10. 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, so RUN npm ci after 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 ci
    • COPY . .

    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.txt before the Python source, go.mod and go.sum before go mod download.

    Two more things help: a .dockerignore so node_modules, .git and 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. 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 of docker run flags, you declare services (each becomes one or more containers) plus the networks and volumes they use.

    Key behaviours:

    • docker compose up -d builds or pulls images, creates a project network and starts everything; docker compose down removes the containers and network, and down -v deletes the volumes too
    • Every service joins the project network, so api reaches the database at the hostname db
    • depends_on controls start order, and with a health-check condition it can wait for readiness
    • Values like ${TAG} are interpolated from the shell environment or a .env file

    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 -d starts the stack, down removes it
    • Services reach each other by service name
    • depends_on controls start order
    • Single-host tool; Kubernetes for clusters

    Likely follow-up: What is the difference between docker compose and the old docker-compose?

  12. 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 -slim Debian 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 .dockerignore so .git, node_modules, test data and logs never get copied in
    • Clean up in the same RUN that 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 -R over large trees, which copies every file into a new layer; use COPY --chown instead

    To find the culprit, docker history <image> shows how much each instruction added, and tools like dive let 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 history shows size per instruction

    Likely follow-up: Why does RUN rm big-file as a separate step not reduce the image size?

  13. 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 (-p is 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, -p ignored
    • 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?

  14. 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.11 inside the container, so containers resolve each other by container name, by any --network-alias, and in Compose by service name.

    The default bridge network, 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 --link flag. 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 -p at 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://web
    What 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 -p needed between containers

    Likely follow-up: How can one container be attached to two networks at once?

  15. 15.What is the difference between EXPOSE in a Dockerfile and -p on docker run?easy

    EXPOSE 80 is 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:80 maps host port 8080 to container port 80
    • -p 127.0.0.1:8080:80 binds only on the host's loopback, so other machines can't reach it
    • -P publishes every exposed port to random high host ports, which is the one place EXPOSE has a real effect

    By default, -p binds 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 like ufw see it. For local-only services, such as a dev database, bind to 127.0.0.1.

    Also make sure the app inside listens on 0.0.0.0, not 127.0.0.1; otherwise the port is published but nothing answers.

    What interviewers listen for
    • EXPOSE is documentation only
    • -p host:container publishes a port
    • -P publishes 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 ufw rules?

  16. 16.What is the difference between ARG and ENV in a Dockerfile?mid

    Both define variables, but they live for different amounts of time:

    • ARG is a build-time variable. You set it with docker build --build-arg APP_VERSION=1.2, later instructions in that stage can use it, and it's not in the running container's environment
    • ENV is stored in the image config, so it's available to later build steps and to every container at run time, where docker run -e can override it

    Scope gotchas: an ARG declared before FROM can only be used in FROM lines; redeclare it inside the stage to use it there. Each stage of a multi-stage build needs its own ARG declarations.

    Neither is safe for secrets. ENV values are baked into the image config, and build-arg values appear in docker 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 ARG into an ENV to 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. 17.What is the build context, and why should you add a .dockerignore file?easy

    In docker build ., the . is the build context: the files the builder can read for COPY and ADD. A .dockerignore file 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: .env files, 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 as build.Dockerfile.dockerignore next to build.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 .dockerignore affect files mounted as volumes at run time?

  18. 18.A container exits immediately or keeps restarting. How do you debug it?mid

    I work from the outside in:

    • docker ps -a shows the state and exit code, such as Exited (1) or Restarting
    • docker logs shows 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 database
    • docker inspect gives State.ExitCode, State.OOMKilled, State.Error and 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 exec into, override the entrypoint with a shell and run the start command by hand
    • docker cp copies files out of a stopped container, and docker events shows 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 error from 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 containers
    What interviewers listen for
    • docker ps -a for state and exit code
    • docker logs works on stopped containers
    • docker inspect for 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. 19.Describe the container lifecycle. How do docker stop, docker kill and docker rm differ?easy

    A container moves through a few states: created (docker create: config and writable layer exist, nothing running), running (docker start, or docker run, which is create plus start), paused (docker pause freezes 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 stop sends the stop signal (SIGTERM unless the image sets STOPSIGNAL), waits a grace period, 10 seconds by default for Linux containers, then sends SIGKILL. That gives the app a chance to shut down cleanly
    • docker kill sends SIGKILL straight away by default, or any signal you choose with -s
    • docker rm deletes a stopped container and its writable layer; -f kills and removes a running one

    A stopped container still exists, keeps its writable layer and logs, and can be started again. Use docker run --rm for throwaway containers so they're removed when they exit.

    What interviewers listen for
    • Created, running, paused, exited, removed
    • docker run is 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. 20.What restart policies does Docker support, and how do always and unless-stopped differ?easy

    A restart policy tells the Docker daemon what to do when a container's main process exits:

    • no (default): never restart automatically
    • on-failure[:max-retries]: restart only after a non-zero exit code, optionally capped, e.g. on-failure:5
    • always: 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 yourself
    • unless-stopped: like always, 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 with docker update --restart, or use restart: 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. 21.In Compose, your API starts before Postgres is ready and crashes. Doesn't depends_on fix that?mid

    Plain depends_on: [db] only controls start order. Compose starts the db container 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 form
    • service_healthy: wait until the dependency's health check passes. The dependency needs a healthcheck, defined in Compose or in its image
    • service_completed_successfully: wait for a one-off job, like a migration container, to exit with code 0

    With service_healthy, docker compose up shows the database as Waiting, then Healthy, and only then starts the API.

    Even so, the app should still retry connections with back-off. depends_on only matters at startup: if the database restarts later, or you deploy to Kubernetes where there's no depends_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: 10
    What interviewers listen for
    • Short form only orders container startup
    • service_healthy waits for the health check
    • service_completed_successfully for 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. 22.What does the HEALTHCHECK instruction do, and what happens when a container becomes unhealthy?mid

    HEALTHCHECK tells 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 code 0 means healthy, 1 means unhealthy.

    The status starts as starting, becomes healthy after a passing check, and turns unhealthy after --retries consecutive failures. The defaults are a 30s interval, 30s timeout, 0s start period and 3 retries. Failures during --start-period don't count, which gives slow apps time to boot. docker ps shows the status and docker inspect shows 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 ignores HEALTHCHECK and 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 1
    What interviewers listen for
    • Exit 0 is healthy, 1 is unhealthy
    • Status: starting, healthy, unhealthy
    • Defaults: 30s interval, 30s timeout, 3 retries
    • --start-period gives 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. 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 node in the official Node images
    • Switch with USER, ideally by numeric UID, so Kubernetes' runAsNonRoot check can verify it
    • Use COPY --chown only 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?

  24. 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 stats reads.

    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 --privileged dangerous?

  25. 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 and OOMKilled: true
    • --memory-swap is memory plus swap; setting it equal to --memory means no swap
    • --cpus 1.5 caps CPU time at one and a half CPUs' worth. Going over means throttling, not killing
    • --cpu-shares is a relative weight that only matters when CPUs are contended
    • --pids-limit caps 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. Use docker stats to 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-swap equal to --memory disables 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. 26.Why does docker stop sometimes hang for 10 seconds and then kill the app? What is special about PID 1 in a container?hard

    The 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 SIGTERM and SIGINT, are ignored unless the process installs a handler. PID 1 also adopts orphaned processes and is expected to reap zombies.

    docker stop sends SIGTERM, waits a grace period (10 seconds by default), then sends SIGKILL. If PID 1 ignores SIGTERM, you get a slow stop, exit code 137 and no clean shutdown. Common causes:

    • Shell-form CMD or ENTRYPOINT, where /bin/sh -c is PID 1 and doesn't forward the signal
    • An entrypoint script that starts the app without exec
    • An app with no SIGTERM handler

    Fixes: use exec form, end entrypoint scripts with exec "$@", handle SIGTERM in the app (stop accepting work, drain requests, close connections, exit), and add a minimal init like tini with docker 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 "$@"
    • --init runs tini to forward signals and reap zombies

    Likely follow-up: What is a zombie process, and why might they pile up in a container?

  27. 27.What is the difference between shell form and exec form for RUN, CMD and ENTRYPOINT?mid

    Shell 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 CMD and docker run arguments
    • Minimal images: shell form needs /bin/sh, which scratch and 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 for CMD and ENTRYPOINT.

    # 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 one
    What 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?

  28. 28.What is the difference between an image tag and a digest? Why is deploying latest a bad idea?mid

    A tag is a human-friendly, mutable pointer in a repository: nginx:1.27 today may point to a different image next week after a rebuild with security patches. latest is just the tag Docker uses when you don't specify one. It isn't automatically the newest version and has no special meaning to the registry.

    A digest, like nginx@sha256:..., is the content hash of the image manifest, or of the index for multi-platform images. It's immutable: the same digest always means exactly the same content, and the client verifies it when pulling.

    In practice:

    • Don't deploy latest: you can't tell what's running, nodes may pull different images, and rollbacks are unreliable
    • Tag your own images uniquely, with a Git SHA or semantic version, and never re-push an existing tag
    • Pin base images by digest, e.g. FROM node:22-bookworm-slim@sha256:..., for reproducible builds, and let a bot like Renovate or Dependabot bump it
    • docker images --digests and docker buildx imagetools inspect show digests
    What interviewers listen for
    • Tags are mutable pointers
    • latest is only the default tag name
    • Digests are immutable content hashes
    • Pin by digest for reproducible builds
    • Use unique tags, like Git SHAs, for releases

    Likely follow-up: How do multi-platform images share one tag across architectures?

  29. 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, so nginx:alpine really means docker.io/library/nginx:alpine.

    The workflow:

    • docker login authenticates to the registry
    • docker tag gives the local image a name that includes the target registry
    • docker push uploads only the layers the registry doesn't already have
    • docker pull downloads 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. 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 inspect on 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 ENV in 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 _FILE convention, such as POSTGRES_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=secret so 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
    • _FILE convention 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?

  31. 31.How do you choose between full, -slim, Alpine and distroless base images?mid

    The base image is usually the biggest factor in both size and attack surface:

    • Full Debian or Ubuntu tags like node:22 or python:3.12: compilers and common libraries included. Easiest to work with, but large and with many packages to patch. Good for build stages
    • -slim variants: 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 :debug variants 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
    • scratch for static binaries

    Likely follow-up: Why can a Python image take much longer to build on Alpine?

  32. 32.What happens, step by step, when you run docker run -d -p 8080:80 nginx?mid

    Docker is client-server. The docker CLI sends REST API calls to the Docker daemon, dockerd, usually over the Unix socket /var/run/docker.sock. Then:

    • The daemon looks for nginx:latest locally 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 dockerd restarts?

  33. 33.What is the difference between docker exec and docker attach?easy

    docker exec starts a new process inside an already running container, sharing its namespaces, filesystem and network. docker exec -it api sh is 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 attach connects 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 run is different again: it creates a new container from an image.

    In practice: exec to investigate, logs -f to watch output, and attach only 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. 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 logs reads it back, with -f to follow, --tail, --since and -t for timestamps.

    Where logs end up depends on the logging driver:

    • json-file is the default. It writes JSON lines on the host and does no rotation by default, so a chatty container can fill the disk. Set the max-size and max-file options, or switch to the local driver, which Docker recommends because it rotates by default and uses a more compact format
    • Other drivers ship logs elsewhere: syslog, journald, fluentd, awslogs, gcplogs, splunk and more
    • With remote drivers, dual logging keeps a local cache so docker logs still works

    Set the default in daemon.json, or per container with --log-driver and --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.json not affect existing containers?

  35. 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 -v for detail, to see which category is the problem.

    Then use the matching prune command:

    • docker container prune removes stopped containers
    • docker image prune removes only dangling images, the untagged <none> images left behind when a tag moves to a new build. Add -a to remove every image not used by a container
    • docker builder prune clears the build cache, often the biggest item on CI machines
    • docker system prune does containers, unused networks, dangling images and build cache in one go

    Volumes are never removed by default, because they hold data. docker volume prune removes unused anonymous volumes, and -a includes named ones, so check before running it.

    On CI, prune with --filter until=24h to 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 for
    • docker system df shows usage by type
    • image prune removes dangling; -a all unused
    • docker builder prune for 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. 36.Explain the common docker run flags: -d, -it, --rm, --name, -e, -p and -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
    • -i keeps stdin open and -t allocates a pseudo-terminal. Together, -it gives you an interactive shell
    • --rm removes the container when it exits, perfect for one-off commands so stopped containers don't pile up
    • --name sets 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=value sets an environment variable, and --env-file loads many from a file
    • -p host:container publishes a container port on the host
    • -v source:target mounts a named volume or host path; --mount is the more explicit equivalent

    Anything after the image name replaces the image's CMD, so docker run --rm nginx:alpine nginx -v prints the nginx version instead of starting the server.

    What interviewers listen for
    • -d runs in the background
    • -it for an interactive terminal
    • --rm removes the container on exit
    • -e, -p, -v for env, ports and storage
    • Arguments after the image replace CMD

    Likely follow-up: What happens if you run docker run -d ubuntu with no command?

  37. 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 ALL and add back only what's needed
    • A --read-only root filesystem, with tmpfs for scratch paths
    • --security-opt no-new-privileges to block privilege escalation through setuid binaries
    • Keep the default seccomp and AppArmor or SELinux profiles, and never use --privileged or mount /var/run/docker.sock casually
    • 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.0
    What 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 --privileged and the Docker socket

    Likely follow-up: A scanner reports 200 CVEs in your image. How do you triage them?

  38. 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 run command 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 inspect shows OOMKilled: true), docker kill, or docker stop giving up after its grace period
    • 143 = 128 + 15, SIGTERM: the process was terminated by the signal docker stop sends. An app that handles SIGTERM itself 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 SIGTERM quickly 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. 39.Your app runs fine in a container, but curl localhost:8080 on the host gets "connection refused". What do you check?easy

    I'd go through these in order:

    • Is the port published? docker ps or docker port <name> should show something like 0.0.0.0:8080->3000/tcp. EXPOSE alone 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.1 inside 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 on 0.0.0.0. Many dev servers default to localhost and need a --host 0.0.0.0 flag
    • Is it running and listening? Check docker logs, and test from inside with docker 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: localhost inside 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?

  40. 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 stop shuts 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. 41.What is the difference between docker save and docker export?mid

    They work on different objects and produce different archives:

    • docker save takes one or more images and writes a tar with all their layers, config and tags. docker load restores them exactly, with history, CMD, ENV and tags intact. It's the way to move images to an air-gapped machine or cache them without a registry
    • docker export takes 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 import turns that tar into a new single-layer image with no CMD, ENV or entrypoint unless you add them with --change

    Neither includes volume contents; volume data lives outside the container's filesystem.

    So: save and load to transport images faithfully, export and import to 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. 42.How does Podman differ from Docker?mid

    Both build and run OCI containers with a nearly identical CLI: podman run takes the same flags as docker 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: each podman command 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?

  43. 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:

    • dockerd provides 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. 44.Your build needs a private npm token. Why is ARG NPM_TOKEN a bad idea, and how should you pass it instead?hard

    Build args and ENV values are recorded in the image: build-arg values show up in docker history, and ENV is stored in the image config. Copying an .npmrc in 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 RUN step, at /run/secrets/<id> by default or wherever target points, 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_TOKEN exposes it as an environment variable for that one command instead of a file.

    For Git over SSH, use RUN --mount=type=ssh with docker build --ssh default, which forwards your SSH agent instead of copying keys. Compose supports the same through build.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=secret exposes it to one step only
    • Pass it with docker build --secret
    • Use --mount=type=ssh for SSH keys

    Likely follow-up: How would you verify that a secret did not end up in the final image?

  45. 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.json changes, the npm ci layer is rebuilt from scratch and every package is downloaded again.

    A cache mount gives a RUN step 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/mod or /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 sharing option (shared, private or locked) controls concurrent builds; locked is common for apt, which can't handle parallel access
    • It lives in the builder's local cache, so docker builder prune clears 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=locked for 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. 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 overlay2 storage 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. 47.Why is running a container with --privileged, or mounting /var/run/docker.sock into it, dangerous?hard

    Both effectively remove the container boundary.

    --privileged gives 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
    • --privileged grants all capabilities and devices
    • It lifts seccomp and AppArmor/SELinux confinement
    • Docker socket access is root on the host
    • Use targeted --cap-add or --device instead
    • Use rootless or remote builders in CI

    Likely follow-up: What is the difference between Docker-in-Docker and mounting the host socket?

  48. 48.What is Docker rootless mode, and how is it different from just setting USER in the Dockerfile?hard

    They're separate layers of defence. Setting USER makes 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 needs newuidmap and newgidmap plus subordinate ID ranges in /etc/subuid and /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. 49.In Compose, what is the difference between the .env file and env_file:? Which value wins if a variable is set in several places?mid

    They solve different problems:

    • The .env file in the project directory is read by Compose itself for interpolation: it fills in ${TAG} style references in compose.yaml. Its values are not automatically passed into containers
    • env_file: on a service lists files whose variables are injected into that service's containers
    • environment: 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 -e on the command line, then environment:, then env_file:, then the image's ENV. For interpolation, variables from your shell override those in .env.

    To check, docker compose config prints the fully resolved file, and docker compose exec <service> env shows what the container actually sees. Keep .env files with real credentials out of Git, and don't confuse either mechanism with Compose secrets, 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_file
    What interviewers listen for
    • .env feeds ${VAR} interpolation in the Compose file
    • env_file injects variables into containers
    • Precedence: CLI, environment, env_file, image ENV
    • Shell variables override .env for interpolation
    • Verify with docker compose config

    Likely follow-up: How would you keep separate settings for dev and production with Compose?

  50. 50.An image built on an Apple Silicon Mac fails in production with exec format error. Why, and how do you fix it?hard

    Images are built for a specific CPU architecture. An Apple Silicon Mac builds linux/arm64 images by default, while most x86 servers need linux/amd64. When the kernel is asked to execute a binary for the wrong architecture, it fails with exec 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=$BUILDPLATFORM and use the automatic TARGETOS and TARGETARCH build 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-container builder. docker buildx imagetools inspect shows which platforms a tag supports.

    What interviewers listen for
    • Images are built per CPU architecture
    • exec format error means an architecture mismatch
    • Use --platform or 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?

  51. 51.What do WORKDIR and LABEL do, and why is RUN cd /app not the same as WORKDIR /app?easy

    WORKDIR sets the working directory for the instructions that follow, such as RUN, CMD, ENTRYPOINT, COPY and ADD, and for the running container. If the directory doesn't exist, it's created. A relative path is resolved against the previous WORKDIR, and you can use it as many times as you like.

    RUN cd /app doesn't persist: each RUN starts a fresh shell, so the cd only applies inside that one command. That's why Dockerfiles that rely on cd end up with long cd x && ... chains, and why WORKDIR with absolute paths is the clearer, recommended approach.

    LABEL adds key-value metadata to the image config without adding any files. Common uses are the standard OCI keys like org.opencontainers.image.source, version and revision, plus team or ownership labels. You can read them with docker inspect and filter with --filter label=..., and registries and scanners use them to link an image back to its source. LABEL replaces the deprecated MAINTAINER instruction.

    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 /app
    What interviewers listen for
    • WORKDIR sets the directory for later instructions
    • It creates the directory if missing
    • RUN cd only 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. 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 profiles attribute is enabled only when one of its profiles is active; services without profiles are always enabled.

    You activate profiles with:

    • docker compose --profile debug up, repeatable for several profiles
    • the COMPOSE_PROFILES=debug,tools environment variable

    Explicitly targeting a service also starts it even if its profile isn't active, so docker compose run --rm seed works 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 up stays 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 -f or the include element usually fit better than profiles.

    What interviewers listen for
    • Profiles make services optional
    • Services without profiles always start
    • Activate with --profile or 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. 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

    localhost inside a container is the container itself, so localhost:5432 won'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, or extra_hosts in Compose. host-gateway is 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, so localhost really is the host, at the cost of isolation

    Don't forget the host side. On Linux, a service bound only to 127.0.0.1 won'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 5432
    What interviewers listen for
    • Container localhost is not the host
    • Docker Desktop provides host.docker.internal
    • On Linux use --add-host with host-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?

  54. 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 :z or :Z to the mount

    Avoid chmod 777 or 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 --user or fix host ownership
    • Named volumes keep the image ownership
    • SELinux hosts may need :z or :Z

    Likely follow-up: Why do files created by a container in a bind mount end up owned by root on the host?

  55. 55.How do you debug a running container built from a distroless or scratch image that has no shell?hard

    docker exec -it api sh fails because there's no sh in the image. Options, from least to most invasive:

    • Look from outside first: docker logs, docker inspect, docker top, docker stats, and docker cp to copy files out
    • Attach a debug container that shares the target's namespaces. With --pid container:api you see its processes, and with --network container:api your localhost is 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 :debug tags 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/app
    What interviewers listen for
    • No shell means docker exec sh fails
    • Start with logs, inspect, top and cp
    • Share PID and network namespaces with a tools container
    • Browse the filesystem via /proc/1/root
    • docker debug or :debug image variants

    Likely follow-up: Why does /proc/1/root only work if the debug container shares the PID namespace?

  56. 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-to and import it on the next run with --cache-from:

    • registry: stores the cache as a separate artifact in your registry, and works anywhere
    • inline: embeds cache metadata in the pushed image; simple, but it doesn't scale well to multi-stage builds
    • gha: uses the GitHub Actions cache service
    • local, s3 and azblob: a directory or object storage

    mode=min, the default, caches only the layers that end up in the final image; mode=max also 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 default docker driver supports these backends only with the containerd image store, otherwise use a docker-container builder. 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=max also caches intermediate stages
    • A cache-friendly Dockerfile still comes first

    Likely follow-up: What are the trade-offs between mode=min and mode=max?

  57. 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 encrypted encrypts 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
    • --attachable lets 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?

Prefer multiple choice? All 25 Docker MCQs with answers →

esc