Docker & Containers
A virtual machine was the answer to wasting a whole physical server on one application; a container is the answer to wasting a whole virtual machine. This chapter rebuilds Days 23 to 29 into a workshop: first you understand what a container really is and why it is so much lighter than a VM, then you get hands-on with Docker — writing your first image, shrinking it with multi-stage builds, persisting data with volumes, isolating containers with networks, and running a multi-container app with Compose — then you get quizzed the way an interviewer would, and finally you face the senior scenarios where image size, security, and runtime behaviour decide whether a container is production-ready.
A container packages an application together with everything it needs to run, so it runs the same on any machine. Unlike a virtual machine, a container does not carry a full operating system — it holds only your application, its libraries, and the minimum system dependencies, and it shares the host's kernel for everything else. That is why a container is measured in megabytes where a VM is measured in gigabytes, and why you can pack dozens of containers onto one machine. Docker is the platform that builds and runs them: you write a Dockerfile (a recipe), Docker builds it into an image (a snapshot), and Docker runs the image as a container (a live process). Share the image through a registry and anyone can run your app with a single command.
Four ideas carry the chapter: (1) a container is lightweight because it has no full OS — just the app, its libraries, and minimal system dependencies over a shared kernel; (2) Docker turns a Dockerfile → image → container through build and run, all handled by the Docker daemon; (3) the everyday craft is making images small and secure (multi-stage builds, distroless), persistent (volumes), and isolated (networks); and (4) Compose runs a whole multi-container app from one file — which is where the senior questions about size, security, and runtime behaviour begin.
- ① Theory — the concept (Day 23–24)
- From VMs to containers — why containers exist
- Why containers are lightweight (the deep dive)
- Docker — the platform and its architecture
- The Docker lifecycle and terminology
- ② Lab — hands-on with Docker (Day 24–29)
- Install Docker and fix the permission gotcha
- Your first Dockerfile — build, run, push
- Multi-stage builds & distroless images
- Volumes & bind mounts — persistent data
- Networking — bridge, host, and isolation
- Docker Compose — multi-container apps
- ③ Interview Q&A — the questions the instructor flags
- ④ Scenario drills — Docker under pressure (15)
From VMs to containers — why containers exist
Containers are the next advancement after virtual machines, so the clearest way in is to see the problem they solve, exactly as VMs solved the problem before them.
A physical server is rarely used to its full capacity — buy a machine with 100 GB of RAM and 100 CPUs for one application, and most of it sits idle while you still pay for all of it. Virtualization solved that: a hypervisor slices one physical server into many virtual machines, each with its own operating system, so you can run many applications on one box and use the hardware far better. This is exactly what an EC2 instance is — AWS runs a hypervisor (Xen) on its physical servers and hands you virtual machines.
But virtual machines have the same waste problem one level down. Give each VM 25 GB of RAM and you soon find that on its busiest day an application uses only 10 GB and 6 CPUs — the rest is wasted, and wasted even more when the application is idle. For one free EC2 instance that does not matter; for an organisation running a million instances, it is an enormous loss. Containers are the answer: they let you use a single virtual machine far more efficiently by running many isolated applications on it, each taking only what it needs.
A container is best defined as a package: your application, plus its application libraries (dependencies), plus the minimum system dependencies it needs — and nothing else. Everything else (the kernel, the networking stack, most of the operating system) it borrows from the host. That is the crucial difference from a VM, which carries a complete guest operating system. It shows up starkly in size: a snapshot of a VM might be 1–2 GB, while a container image is often tens or hundreds of megabytes. Because they are so small, containers are also easy to ship — to move from a laptop to a server, or onto Kubernetes — which is a large part of their appeal. (Containers are not perfectly isolated the way VMs are, since they share the host kernel — a trade-off we will keep meeting.)
Why containers are lightweight (the deep dive)
"Containers don't have an operating system" is the phrase everyone repeats and few can explain. Here is what it actually means — and why it is a favourite interview question.
A container is not truly OS-less; it carries a small base image with a minimal set of system files and folders, and shares the rest with the host. The files it keeps are the ones that create its logical isolation from other containers — the basic filesystem layout such as /bin (binaries), /sbin (system binaries), /etc (configuration), /lib (libraries), /usr, /var (logs), and /root. These are not shared, precisely so that project A's container cannot reach into project B's — if they were shared, a hacker who breaks into one container could reach every container on the host.
What a container does take from the host is everything in the kernel: the host filesystem, the networking stack, system calls, namespaces, and control groups (cgroups). These are the heavy, shared parts of an operating system, and because the container borrows rather than duplicates them, it stays tiny. Concretely, the official Ubuntu container image is about 28 MB, while a Ubuntu VM image is around 2.3 GB — roughly 100× smaller. That is why one virtual machine can host dozens of containers: as long as a container is idle it uses almost nothing from the kernel, leaving room for the others.
Docker — the platform and its architecture
Containerization is the concept; Docker is the platform that implements it. Once you understand containers, Docker is just the tool that creates them.
If containerization is a concept, Docker is a platform that implements that concept — an open-source containerization platform used to build containers and manage their lifecycle. It is not the only one (podman is a strong competitor), but it is the easiest to start with, so learn Docker first and reach for podman or buildah later. Docker's architecture has three parts you interact with. You, the user, run commands through the Docker client (the CLI). Those commands are received by the Docker daemon (also called dockerd) — the process you actually install when you "install Docker," and the heart of the whole system. And the daemon can push and pull images to a registry.
docker build/run/pull in the client; the daemon carries them out — building images, running containers, and pushing or pulling from a registry.Two properties of the daemon come up again and again, both as strengths and as weaknesses. The daemon is a single process and the heart of Docker: if it goes down, every container it manages stops — a single point of failure. And it runs as the root user: convenient, but a security concern, because anyone who compromises the daemon effectively has root on the host. These are exactly the drawbacks that newer tools such as podman (daemonless, rootless) and buildah were built to address — worth knowing, though Docker remains the place to start.
The Docker lifecycle and terminology
Two more things make the rest of the chapter easy: the short lifecycle every image goes through, and the handful of terms the community uses.
The lifecycle has three stages. You write a Dockerfile — a set of instructions: take a base image, copy in the source code, run the commands that install dependencies, and set the command that starts the app. You turn that into an image with docker build (an image is like a VM snapshot — a frozen, runnable package). And you turn the image into a running container with docker run. That is the whole flow: Dockerfile → (build) → image → (run) → container. The image can then be shared through a registry so anyone can pull and run your application without installing a single dependency themselves — which is Docker's other great gift beyond lightness: it collapses a hundred manual setup steps into one.
build and run — carry you the whole way.A few terms tie it together. The daemon is the background service that executes your requests. The client is the CLI you type into. A registry is where images are stored and shared — Docker Hub is the popular public one (Quay.io, ECR, and GCR are others), and it can hold public or private repositories. A common interview question is the difference between GitHub and Docker Hub: both are version-control platforms, but GitHub stores source code while Docker Hub stores images. With the concept clear, the rest is just using the platform — which is what the Lab does next.
Install Docker and fix the permission gotcha
On an Ubuntu EC2 instance, installing Docker is two commands. (On your own laptop, install Docker Desktop instead — it quietly sets up a VM and Docker for you.) Verify the daemon is running:
bashsudo apt update
sudo apt install docker.io -y
sudo systemctl status docker # should say "active (running)"
Now the classic beginner trap. Run a container as your normal user and you get permission denied:
bashdocker run hello-world # ✗ permission denied — the daemon runs as root
This happens because the Docker daemon runs as root, so your ubuntu user cannot talk to it. The fix is to add your user to the docker group, then log out and back in so the new group membership takes effect:
bashsudo usermod -aG docker ubuntu # add the ubuntu user to the docker group
# log out and log back in (your group membership only refreshes on a new login)
docker run hello-world # ✓ now it works
Almost everyone hits this on their first day; it is not a broken install, just a group-membership step. If you would rather not log out, sourcing your profile works too, but logging back in is the simplest fix.
Your first Dockerfile — build, run, push
Containerise a tiny Python app so anyone can run it with one command, no matter what is (or isn't) installed on their machine.
Say app.py just prints "hello world." To run it the old way, someone must create a machine, install Python, install pip, install every dependency, and hope the versions line up. With Docker, they run one command. The recipe is a Dockerfile with a handful of instructions — a base image, a working directory, the source copied in, the dependencies installed, and the command that starts the app:
dockerfileFROM ubuntu:latest # base image — pulled from Docker Hub
WORKDIR /app # work inside /app (optional but tidy)
COPY . /app # copy the source (app.py) into the image
RUN apt-get update && apt-get install -y python3 # install the dependency
CMD ["python3", "app.py"] # the command run when the container starts
Build it into an image with a tag (always tag — otherwise you get an unmemorable image ID), run it, then log in and push it to Docker Hub so others can pull it:
bashdocker build -t abhishekf5/my-first-docker-image:latest . # . = Dockerfile in this dir
docker run -it abhishekf5/my-first-docker-image:latest # prints "hello world"
docker login # with your Docker Hub username/password
docker push abhishekf5/my-first-docker-image:latest # share it to the registry
Now anyone in the world can run docker pull abhishekf5/my-first-docker-image and then docker run it — with none of the dependencies installed on their side, because the image already carries them. The tag has the form username/repository:version, mirroring how GitHub names things; after the push, a matching repository and tag appear under your Docker Hub account. For a web app the flow is identical — the app simply runs and you reach it in a browser via a mapped port.
Multi-stage builds & distroless images
The single most impactful Docker skill — and a favourite "what production problem did you solve?" answer. It shrinks images dramatically and makes them far more secure.
The single-stage image above has a flaw: it ships everything used to build the app, when running it needs far less. Building a Java app needs the JDK and many libraries; running it needs only the JRE plus the built .jar. A multi-stage build separates the two. You use one Dockerfile with two (or more) FROM stages: a rich build stage where you install everything and compile, and a lean final stage that starts from a minimal base and copies only the finished artifact across with COPY --from. All the build tooling stays behind in the discarded first stage:
dockerfile# ---- stage 1: build (rich, can be huge — it gets thrown away) ----
FROM ubuntu AS build
RUN apt-get update && apt-get install -y golang-go
COPY calculator.go .
RUN go build -o /calculator calculator.go # produce the binary
# ---- stage 2: final (tiny — only the artifact) ----
FROM scratch # the most minimal base there is
COPY --from=build /calculator /calculator # copy ONLY the binary across
ENTRYPOINT ["/calculator"]
The payoff is dramatic. In the course demo, the single-stage Go image was 861 MB; the multi-stage version was 1.83 MB — roughly 800× smaller. The final stage used a distroless image: a minimal image that contains only the runtime and none of the usual OS tooling — no shell, no apt, no curl, no ls or find. Go is special because it compiles to a static binary that needs no runtime at all, so it can use scratch (the emptiest base). For Python or Java, use their distroless images (which include just the Python or Java runtime).
Volumes & bind mounts — persistent data
The topic where people feel Docker gets hard. It is not — you just have to see the problem first.
Containers are ephemeral — short-lived. Because a container borrows its filesystem from the host and gives it back when it dies, anything written inside the container is lost when the container goes down. That breaks three common situations: an nginx container whose access logs vanish (so you have nothing to audit); a backend that writes files a frontend needs to read (gone when the backend restarts); and a container that needs to read a file a host cron job produces (which it has no standard way to reach). The cure is to attach persistent storage from the host, and Docker offers two ways.
A bind mount binds a specific host directory to a directory inside the container — write to /app in the container and it lands in the host folder, surviving the container and available to the next one. A volume does the same job but is managed by Docker: instead of naming a host path, you ask Docker to create a named volume, and it gets a proper lifecycle you drive from the CLI. Prefer volumes — they can live on external storage (another host, NFS, S3), be backed up, be shared between containers, and be created, inspected, and removed with simple commands:
bashdocker volume create abhishek # create a Docker-managed volume
docker volume ls # list volumes
docker volume inspect abhishek # where it lives, driver, mount point
docker run -d --mount source=abhishek,target=/app nginx:latest # mount it into a container
docker inspect <container> # confirm the mount under "Mounts"
Two practical notes. You mount with either -v (compact, everything in one comma-separated string) or --mount (verbose, spelled-out source=…,target=…); they do the same thing, but prefer --mount in a team because it reads clearly. And to delete a volume you must first stop and remove the container using it — Docker refuses to remove a volume that is in use.
Networking — bridge, host, and isolation
Containers need to talk to each other and to the outside world — and sometimes to be kept firmly apart. Docker networking is how you control both.
A container has its own subnet (say 172.17.0.2) while the host is on another (say 192.168.x), so by default they cannot reach each other — and a container nobody can reach is useless. Docker fixes this by creating a virtual ethernet bridge, docker0, whenever you start a container. This is bridge networking, the default: every container connects to docker0, which lets it talk to the host (and, with a port mapping, to the outside world) and to other containers on the same bridge. There are two other modes. Host networking makes the container share the host's network directly — simple, but insecure, because anyone who reaches the host reaches the container. Overlay networking spans multiple hosts and matters for clusters (Docker Swarm, Kubernetes); it is overkill for a single Docker host.
docker0 and can reach the others. Put a sensitive container on its own custom bridge network and it is cut off from the rest.The default bridge has a catch: every container shares docker0, so they can all reach one another — fine for a frontend and backend that must talk, but wrong for a payments container that should be sealed off from a basic login container. The fix is a custom bridge network: create your own bridge and attach the sensitive container to it, breaking the shared path. In the demo, login and logout on the default bridge can ping each other, but a finance container on a custom bridge cannot be reached from them:
bashdocker network ls # bridge, host, none, + any custom ones
docker network create secure-network # a custom bridge network
docker run -d --name finance --network=secure-network nginx # attach the container to it
docker inspect finance # its network is secure-network, on a separate subnet
This need to hand-craft networks and port mappings for every container — and the questions of what happens when a container crashes or needs to scale — is exactly what motivates Kubernetes, the next chapter.
Docker Compose — multi-container apps
One command to start a whole application made of many containers.
Docker manages a single container's lifecycle. Real applications are not single containers — an e-commerce app might be several microservices plus a database and a cache. Starting those by hand means many docker build and docker run commands, in the right order (the database before the app that needs it), often wrapped in a shell script that quietly breaks over time. Docker Compose (also from Docker) replaces all that with one declarative YAML file describing every service, and two commands to run the lot:
yaml# docker-compose.yml
services:
web1:
build: ./web # build from a Dockerfile…
ports: ["81:5000"] # host:container port mapping (same as -p)
web2:
build: ./web
ports: ["82:5000"]
nginx:
build: ./nginx
ports: ["80:80"]
depends_on: [web1, web2] # ordering — start web1/web2 first
redis:
image: redis:latest # …or just use a published image
bashdocker compose up # build & start every service, in dependency order
docker compose down # stop and remove them all
The magic is that anyone — a developer, a tester — only needs to know docker compose up and docker compose down; the DevOps engineer does the heavy lifting in the file once. Compose does not replace Docker: you still write Dockerfiles, and Compose's build: points at them (or image: uses a published one). The main fields are just services with, under each, build/image, ports, depends_on, and optionally networks. Compose shines for local development, lightweight CI/CD (spin the app up to test a pull request without a Kubernetes cluster), and quick testing.
compose up is great for development but is a single point of failure in production, which is where you reach for a real orchestrator.Q1What is Docker (and what is a container)?reveal ▸
Docker is an open-source containerization platform used to build containers and manage their lifecycle. Interviewers usually care more about the container than the platform, so answer both: a container packages an application with its libraries and minimal system dependencies, sharing the host kernel, so it is lightweight and runs the same anywhere; Docker is the tool you use to build the image, run the container, and push it to a registry. Then say what you do with it — write Dockerfiles, build images, run containers, push to registries.
Q2How are containers different from virtual machines?reveal ▸
A virtual machine has a complete guest operating system, which makes it heavy — gigabytes in size. A container is lightweight because it has no full OS; it is just the application, its dependencies, and minimal system libraries, sharing the host kernel. Never say a container has "no operating system" — say it has minimal system dependencies for isolation and borrows the kernel from the host. A Ubuntu container image is around 28 MB versus roughly 2.3 GB for a Ubuntu VM — about 100× smaller.
Q3Why are containers lightweight? What do they keep vs share?reveal ▸
They keep only a minimal base — the filesystem layout (/bin, /sbin, /etc, /lib, /usr, /var, /root) plus the app and its libraries — which exists to give each container logical isolation from the others. Everything heavy they borrow from the host kernel: the filesystem, networking stack, system calls, namespaces, and cgroups. Because they borrow rather than duplicate the kernel, they stay tiny, and many containers fit on one VM.
Q4What is the Docker lifecycle?reveal ▸
You write a Dockerfile with the instructions to run the app; you build it into an image with docker build; you run the image as a container with docker run; and you can push the image to a registry (Docker Hub, ECR, Quay) with docker push so others can pull and run it. In short: Dockerfile → image → container, plus push/pull to share.
Q5What are the components of Docker?reveal ▸
The Docker client (the CLI you type commands into), the Docker daemon (dockerd — the process that receives your commands and does the work of building images and running containers; the heart of Docker, and if it goes down, everything stops), and the registry (where images are stored and shared, itself often running as a container). You send a command like docker build through the client; the daemon carries it out.
Q6What is the difference between Docker COPY and ADD?reveal ▸
COPY copies files from your local filesystem (your laptop or the build machine) into the image — for example, your source code. ADD does that too but can also fetch from a URL and auto-extract archives — for example, downloading a file from an S3 bucket or the internet. They are genuinely different; the confusion is only in wording. Use COPY for local files, ADD when you need its remote-fetch or extract behaviour.
Q7What is the difference between CMD and ENTRYPOINT?reveal ▸
CMD provides arguments that can be overridden at docker run time; ENTRYPOINT sets the executable that should not be overridden. The rule: put the fixed part (the thing that must always run) in ENTRYPOINT, and the configurable part (defaults a user might change) in CMD. For example, ENTRYPOINT ["python3","manage.py"] with CMD supplying a default host/port the user can override. You can use either alone or both together.
Q8What are the networking types in Docker, and which is the default?reveal ▸
The default is bridge: Docker creates a virtual ethernet (docker0) through which a container reaches the host, and with a port mapping the outside world reaches the app inside. Host networking binds the container directly to the host's network — no docker0 — which is simple but insecure. Overlay spans multiple hosts, used with Swarm or Kubernetes. Macvlan makes a container appear on the network as a physical host. Use bridge unless you have a specific reason not to.
Q9How do you isolate networking between containers?reveal ▸
By default every container shares docker0, so they can all reach each other — no isolation. To seal one off (say a payments container from a login container), create your own bridge network with docker network create secure-network and start the sensitive container with --network=secure-network. It then talks to the host through its own bridge on a separate subnet, so a compromise of the login container cannot reach it. You can prove the isolation by trying (and failing) to ping across.
Q10What is a multi-stage build?reveal ▸
A multi-stage build builds your image in multiple stages and copies only the finished artifact from one stage into the next. A rich first stage installs all the build tooling and compiles the app; the final stage starts from a minimal base and takes just the built binary or jar via COPY --from, leaving all the build junk behind. It slashes image size — in the demo, an 800 MB image dropped to a couple of MB — because the final image carries only the runtime and the artifact.
Q11What are distroless images?reveal ▸
Distroless images are very minimal images containing only the runtime for your app and none of the usual OS tooling — no shell, no package manager, no curl or ls. A Go app can even use scratch (nearly empty) because it needs no runtime at all. The big win, beyond small size, is security: with almost no packages there are almost no vulnerabilities and almost nothing for an attacker to use even if they get in. Moving to distroless removes a whole class of OS-level vulnerabilities.
Q12What are the real-time challenges you've faced with Docker?reveal ▸
Three good ones. The Docker daemon is a single point of failure — it is one process, and if it goes down every container stops (podman, being daemonless, addresses this). The daemon runs as root, so compromising it can mean root on the whole host (podman is rootless). And resource contention: if you do not set limits, one leaky container can starve the others on the host. Naming real issues like these signals genuine hands-on experience.
Q13What steps would you take to secure containers?reveal ▸
Use distroless images so there are minimal packages and thus minimal vulnerabilities and no shell to exploit. Configure networking properly — put sensitive containers on their own custom bridge so a compromised container cannot reach them. Don't run as root inside the container. And scan images for vulnerabilities with a tool (such as an image scanner in the CI pipeline) before pushing to staging or production. Containers are less isolated than VMs, so these steps matter more.
Q14What are volumes and bind mounts, and why use them?reveal ▸
Containers are ephemeral, so data written inside one is lost when it dies. Both give you persistent storage from the host. A bind mount maps a specific host directory to a container directory. A volume is Docker-managed storage with a proper lifecycle you control from the CLI (docker volume create/ls/inspect/rm); it can live on external storage, be backed up, and be shared between containers. Prefer volumes. To delete a volume, stop and remove the container using it first.
Q15What is Docker Compose, and how does it differ from Kubernetes?reveal ▸
Docker Compose runs multi-container applications from one YAML file — docker compose up starts every service (in dependency order via depends_on), docker compose down removes them. It is ideal for local development, lightweight CI, and quick testing, but it is not an orchestrator: no self-healing, no scaling across hosts, so a single-VM Compose setup is a single point of failure in production. Kubernetes is an orchestrator; the fair Docker-world comparison to Kubernetes is Docker Swarm, not Compose.
Docker
15 scenarios · 1–15The complete Docker scenario set, worked through in depth. This is the thinking and the story behind each one, not commands to memorise. Senior Docker is about image efficiency, security, build reproducibility, and runtime behaviour — layer caching, PID 1, the ephemeral filesystem, cgroups — not docker run. The mental models below make the "why" obvious.
A handful of mental models answer most Docker questions and prevent most mistakes. Burn these in — nearly every scenario is an application of one:
- A container is a process, not a VM. It lives exactly as long as its PID 1 (the main process), runs it in the foreground, and dies when it exits. Most "container won't stay up" issues come from misunderstanding this.
- Image layers are immutable, cached, and permanent. Each Dockerfile instruction is a layer; a changed layer invalidates all below it (so order by change-frequency). And nothing in a layer is ever truly deleted — a secret added and later "removed" is still recoverable from history.
- Containers are ephemeral (cattle). The writable layer dies with the container; persistent state must live in volumes or external stores.
- Minimal images = security + speed. Multi-stage builds and distroless/alpine bases mean fewer bytes to pull and less attack surface.
- Resource limits contain blast radius, and runtimes must be cgroup-aware (a JVM/Node that sizes to the host, not the container, gets OOM-killed).
localhostinside a container is the container itself — not the host, not a sibling. Containers talk by name over a user-defined network.
---
📋 The full scenario inventory (distinct — no padding)
A. Image build & efficiency
- The image is 2 GB — shrink it
- Build cache keeps busting — slow builds
- Slow builds / leaked files from the build context
- Works on my machine, fails on the server (architecture mismatch)
B. Security
- Container runs as root — hardening
- Secrets baked into the image
- Image vulnerabilities / supply chain
C. Runtime behaviour
- Container exits immediately / restarts in a loop
- PID 1, signal handling & zombie reaping
- Data lost when the container restarted
- Container resource limits / noisy neighbour
D. Networking
- Networking between containers isn't working
E. Debugging & host operations
- Debugging a running container with no shell
- Host disk fills up from Docker
F. Appropriateness
- Docker Compose in production — is it appropriate?
---
1The image is 2 GB — shrink itScenario▸
A bloated image that's slow to pull, slow to deploy, expensive to store, and a large attack surface. "Why is our image 2 GB?"
What is actually happening (the mental model).
The image bundles compilers, dev dependencies, source, and build cache alongside the runtime — most of which the running app never touches. A multi-stage build separates the concerns: a builder stage with everything needed to compile, and a lean runtime stage that COPY --froms only the compiled artifact.
How to work through it.
- Multi-stage build — builder stage compiles; final stage
FROMa minimal/distroless/alpine base and copies only the artifact. - Minimise layers and combine
RUNcommands where it reduces cache-busting, and clean up package-manager caches in the same layer they were created. - Use
.dockerignoreso the build context (and accidentally, the image) doesn't pick up.git,node_modules, local env files. - Measure with
docker history/diveto see which layer is actually heavy before guessing.
The root causes and trade-offs.
One cause is a single-stage Dockerfile that ships all its build tooling into production. Another is having no .dockerignore file at all. A third is using a verbose base image, like full Ubuntu or Debian, where something like alpine or distroless would do the job just as well. The trade-off is a slightly less familiar debugging environment, since you lose some shell tools, in exchange for major wins in size and security.
The trap that less-experienced engineers fall into.
Treating image size as unavoidable ("that's just how big the app is") instead of separating build-time and run-time concerns — or switching to alpine and being surprised by subtle glibc/musl compatibility issues without testing.
🎯 Interviewer follow-up questions you should expect.
- "How do you shrink a bloated image?" You use a multi-stage build with a minimal or distroless base, add a
.dockerignore, and measure withdiveordocker history. - "Alpine vs distroless vs full OS?" Alpine is small but its musl-libc can subtly break some binaries; distroless has no shell at all, so it's harder to debug but more secure; a full OS is easiest to debug, but largest and least secure.
- "Why not just delete files at the end of a RUN?" Because layers are additive — deleting in a later layer doesn't remove the bytes from the image, so you either must not create them in the final image's layers at all through a multi-stage build, or delete them within the same layer they were added.
.dockerignore so the build context doesn't leak .git or node_modules in, and I measure with dive rather than guessing which layer is heavy. The security win is as big as the size win — distroless has no shell and almost nothing for an attacker to use."---
2Build cache keeps busting — slow buildsScenario▸
Every CI build reinstalls all dependencies from scratch even though only one source file changed. Builds that should take seconds take minutes.
What is actually happening (the mental model).
Docker caches each layer, but invalidates it — and every layer after it — the moment anything in that instruction changes. If COPY . . (the whole source tree, including a one-line change) comes before RUN npm install, then every source change invalidates the dependency-install layer too, even though dependencies didn't change.
How to work through it.
- Order instructions by change frequency — least-frequently-changed first. Copy dependency manifests (
package.json,requirements.txt) and install before copying the rest of the source. - Separate dependency install from source copy so the expensive install layer stays cached across ordinary code changes.
- Use BuildKit cache mounts for package-manager caches so even a cache-miss install doesn't re-download everything from the network.
Root causes, and how to tell them apart.
Wrong instruction order (source copied before dependency install) is the classic cause. If dependency installs are slow even on a genuine cache hit, that's a different problem — a slow registry/mirror, not cache ordering.
The trap that less-experienced engineers fall into.
COPY . . as the very first instruction "to keep it simple" — which silently busts the cache on every single source change, including whitespace and comments.
🎯 Interviewer follow-up questions you should expect.
- "Why does changing one file reinstall all dependencies?" Because the source copy happened before the install layer, so any source change invalidates it.
- "How do you fix it?" You copy the manifest files and install dependencies first, then copy the rest of the source afterward.
---
3Slow builds / leaked files from the build contextScenario▸
A docker build takes ages just to start ("Sending build context to Docker daemon... 900MB"), and you later find a .git folder or local .env file baked into the image.
What is actually happening (the mental model).
Before Docker executes a single instruction, it packages the entire build context (the directory you pointed docker build at) and sends it to the daemon. Without a .dockerignore, that includes .git, node_modules, local secrets, and anything else sitting in the folder — slow to send, and easy to accidentally COPY into the image.
How to work through it.
- Add a
.dockerignoreexcluding.git,node_modules, local env files, build artifacts — mirror your.gitignoreas a starting point. - Keep the build context minimal — build from a directory that only contains what the image actually needs, rather than the whole repo.
- Audit what's actually in the image with
docker history/diveif you suspect something leaked in already.
The root causes and trade-offs.
No .dockerignore, or a build context set too broad (the repo root instead of a scoped subfolder). The fix is nearly free — a text file — so there's no real trade-off.
The trap that less-experienced engineers fall into.
Assuming COPY only takes what you explicitly reference — it's easy to COPY . . and forget that "." includes everything in the build context, secrets and all.
🎯 Interviewer follow-up questions you should expect.
- "Why is my build context huge?" Because there's no
.dockerignore, so.git,node_modules, and build artifacts are all being sent to the daemon. - "How did a secret end up in the image?" It was sitting in the build context and got swept up by a broad
COPY . ..
.dockerignore — Docker sends the whole build context to the daemon before it runs a single instruction, so .git, node_modules, and local secrets all go along for the ride unless I exclude them. I treat the .dockerignore as seriously as the Dockerfile itself, and keep the build context scoped to only what the image actually needs."---
4Works on my machine, fails on the server (architecture mismatch)Scenario▸
An image built and tested fine on an Apple Silicon laptop fails to start (or silently misbehaves) once deployed to a standard x86_64 server.
What is actually happening (the mental model).
Docker images are architecture-specific. A Mac with Apple Silicon builds arm64 images by default; most cloud servers run amd64. If you never specified a target platform, you built an image for your own machine's architecture, not the one it's actually going to run on.
How to work through it.
- Build for the target platform explicitly —
docker buildx build --platform linux/amd64(or whatever your servers run). - For multi-architecture support, use
buildxto build a multi-arch manifest so the same tag resolves correctly on both arm64 and amd64 hosts. - Make platform an explicit, documented part of the CI pipeline rather than relying on whatever architecture the build machine happens to be.
The root causes and trade-offs.
Building on a different CPU architecture than production, with no explicit --platform flag. The fix costs a slightly slower/cross-compiled build in exchange for correctness.
The trap that less-experienced engineers fall into.
Not realising a laptop's CPU architecture affects the built image at all — chasing a "flaky" runtime bug that's actually just the wrong binary format for the target host.
🎯 Interviewer follow-up questions you should expect.
- "Image works locally, fails on the server — first thing you check?" A CPU architecture mismatch, like an arm64 build laptop versus an amd64 server — you check it with
--platform. - "How do you support both architectures?" You use
buildxto produce a multi-arch manifest under one tag.
buildx --platform, and where I need to support both, I build a proper multi-arch manifest rather than relying on whatever the build machine happens to be."---
5Container runs as root — hardeningScenario▸
A security review flags that your application containers run as root inside the container — if the app is compromised, the attacker has root in that container's namespace.
What is actually happening (the mental model).
By default, unless a Dockerfile says otherwise, the process runs as root inside the container. That's not the same as root on the host, but it still means a container-breakout vulnerability hands the attacker root, and a broad set of accidental capabilities (writing anywhere in the container's filesystem, binding low ports, and more) that the app never actually needs.
How to work through it.
- Create a non-root user in the Dockerfile and switch to it with
USERbefore the app runs. - Drop unnecessary Linux capabilities at run time (
--cap-drop=ALL, add back only what's needed) and consider--read-onlyfor the root filesystem where the app doesn't need to write to it. - In Kubernetes, enforce this centrally with a Pod Security Standard / admission policy rather than trusting every Dockerfile to get it right.
The root causes and trade-offs.
Root is simply the Docker default and nobody opted out. The trade-off is occasionally needing to fix file-permission issues (a non-root user can't write where root could), which is a one-time Dockerfile fix, not an ongoing cost.
The trap that less-experienced engineers fall into.
Assuming container isolation alone is "good enough" security and that root-inside-the-container doesn't matter because "it's just a container" — ignoring that container escapes and shared-kernel vulnerabilities are real.
🎯 Interviewer follow-up questions you should expect.
- "Why does root-in-container matter if it's isolated?" Because container isolation isn't absolute — the kernel is shared, and escape vulnerabilities exist — so root inside the container multiplies the blast radius if that isolation is ever broken.
- "How do you enforce non-root across a whole cluster?" With Pod Security Standards or admission control, rather than relying on every Dockerfile individually.
USER in the Dockerfile and drop capabilities the app doesn't need at run time. Container isolation isn't absolute, so I don't want to multiply the blast radius of a container-escape bug by also handing over root. At the cluster level I'd rather enforce this with a Pod Security Standard than trust every team's Dockerfile individually."---
6Secrets baked into the imageScenario▸
An API key was set with ENV or copied in during the build, then "removed" in a later layer — but it's still recoverable from the image's history.
What is actually happening (the mental model).
Image layers are additive and permanent. A secret written in one layer still exists in that layer even if a later layer deletes the file — docker history or simply extracting the layer tarballs recovers it. "Removing" a secret in a later RUN only hides it from the final filesystem view, not from the image itself.
How to work through it.
- Never
ENV/COPYa real secret into any layer — treat every layer as public once the image is pushed anywhere. - Use BuildKit secret mounts (
--mount=type=secret) which make a secret available only during a singleRUNand never persist it in a layer. - Inject secrets at runtime instead of build time — environment variables or mounted files supplied by the orchestrator (Kubernetes Secrets, a vault), not baked into the image.
- If a secret has already leaked into a pushed image, rotate the secret — treat the image history as compromised, don't just rebuild without the line.
The root causes and trade-offs.
Treating build-time ARG/ENV as if it were ephemeral, when it's actually baked into a permanent layer. The fix (runtime injection, BuildKit secret mounts) costs a little pipeline setup for a real security guarantee.
The trap that less-experienced engineers fall into.
Deleting a secret file in a later Dockerfile line and believing that's sufficient — not realising layers are cumulative and the earlier layer with the secret is still part of the image.
🎯 Interviewer follow-up questions you should expect.
- "Is deleting a secret in a later layer enough?" No — the earlier layer still contains it, and anyone with the image can extract it.
- "How do you use a secret during build without baking it in?" With BuildKit's
--mount=type=secret. - "A secret leaked into a pushed image — now what?" You rotate the secret immediately, and treat it as compromised, not just fixed by a rebuild.
---
7Image vulnerabilities / supply chainScenario▸
A scanner flags dozens of CVEs in your base image, or leadership asks how you know an image pulled from a registry is actually what it claims to be.
What is actually happening (the mental model).
Every layer you didn't write yourself — the base image, and anything it pulled in — is part of your supply chain, and any of it can carry known vulnerabilities or be tampered with. Minimal base images reduce the surface; scanning and provenance catch what's left.
How to work through it.
- Scan images in CI (Trivy, Grype, Snyk) and fail the build on criticals, not just report them after the fact.
- Prefer minimal/distroless base images to shrink the attack surface in the first place.
- Pin base image versions/digests rather than floating tags like
latest, so a base image can't silently change under you. - For provenance, use image signing (Cosign/Notary) so you can verify an image actually came from your pipeline and hasn't been tampered with.
The root causes and trade-offs.
One cause is a large base image with many packages, which increases your CVE exposure simply through surface area. Another is floating tags with no version pinning. A third is having no scanning gate in CI at all. The trade-off is some added friction in CI, from scanning and signing, in exchange for real supply-chain assurance.
The trap that less-experienced engineers fall into.
Scanning only in a dashboard nobody reads, rather than gating the build/deploy pipeline on scan results — or chasing every low-severity CVE while ignoring that the base image itself is needlessly large.
🎯 Interviewer follow-up questions you should expect.
- "How do you catch vulnerable images before they ship?" You scan them in CI, with a tool like Trivy, Grype, or Snyk, and fail the build on anything critical.
- "How do you know an image wasn't tampered with?" Through image signing, with Cosign or Notary, plus pinned digests rather than floating tags.
---
8Container exits immediately / restarts in a loopScenario▸
You run a container and it exits within a second, or in Kubernetes it shows CrashLoopBackOff — the app "should" be running, but the container won't stay up.
What is actually happening (the mental model).
A container's life is tied to its PID 1 — the moment that process exits, the container exits, no matter what else might still be running. The two classic causes: the intended long-running process actually finished or crashed immediately (a config error, a missing dependency, a foreground-vs-background mistake), or the entrypoint script itself exits after doing setup work instead of handing control to the real server process.
How to work through it.
- Check the exit code and logs —
docker logs/docker inspect(the.State.ExitCode) tell you whether it crashed or exited cleanly. - Confirm the main process is meant to run in the foreground — a common mistake is starting a service in daemon/background mode inside the container, after which the entrypoint script has nothing left to do and exits.
- If using an entrypoint script, make sure it ends by
exec-ing the real server process (so it becomes PID 1), not launching it in the background and falling off the end of the script.
The root causes and trade-offs.
Sometimes this is a genuine application-level failure, like bad configuration or a missing dependency. Other times it's simply a foreground-versus-background mistake in how the process was started inside the container. The logs and the exit code together tell the two apart quickly.
The trap that less-experienced engineers fall into.
Backgrounding the main process inside the container (service nginx start style) instead of running it in the foreground — the container exits the instant the launcher script finishes, even though the service technically "started."
🎯 Interviewer follow-up questions you should expect.
- "A container exits immediately — first thing you check?" You check
docker logsand the exit code, then whether the main process is actually running in the foreground. - "Why does the container die even though the service 'started'?" Because the service was launched in the background, so PID 1, the launcher, exited once its own work was done.
docker logs and the exit code to see whether it's an app crash or a process-management mistake. The classic gotcha is starting the real service in background/daemon mode — the entrypoint script then has nothing left to do and exits, taking the container down with it even though the service technically started. The fix is making sure the entrypoint execs the real process in the foreground so it becomes PID 1 itself."---
9PID 1, signal handling & zombie reapingScenario▸
A container takes ages to stop (always hitting the hard kill timeout), or you notice zombie processes accumulating inside long-running containers.
What is actually happening (the mental model).
PID 1 has special responsibilities in Linux that most application processes were never written to handle: forwarding signals like SIGTERM to child processes, and reaping zombie (exited-but-not-collected) child processes. Many applications ignore SIGTERM when running as PID 1, so docker stop waits out the grace period and then hard-kills with SIGKILL — an ungraceful shutdown.
How to work through it.
- Use a minimal init process as PID 1 — Docker's built-in
--initflag (which runstini) — so signal forwarding and zombie reaping are handled correctly, and your app runs as a normal child process. - Make sure your application actually handles
SIGTERMfor graceful shutdown (closing connections, finishing in-flight work) rather than relying on the hard-kill timeout. - If using a shell-script entrypoint, remember
CMDin shell form doesn't forward signals well — prefer exec form orexecthe final process.
The root causes and trade-offs.
Running an app directly as PID 1 without an init process, and/or the app not handling SIGTERM. The trade-off of adding --init is essentially free — it's a tiny, purpose-built binary.
The trap that less-experienced engineers fall into.
Not realising PID 1 is special at all — assuming any process can be PID 1 with no consequences, then being confused why docker stop always takes the full grace period before hard-killing.
🎯 Interviewer follow-up questions you should expect.
- "Why does docker stop always hang and hard-kill?" Because the app running as PID 1 isn't handling or forwarding SIGTERM — you add
--initand make sure the app handles graceful shutdown. - "What does PID 1 have to do that other processes don't?" It has to forward signals to its children and reap any zombie processes.
docker stop always waits out the grace period and hard-kills, that's usually the app ignoring SIGTERM as PID 1. I run with Docker's --init flag so a tiny init process handles that correctly and the app runs as a normal child, and I make sure the app itself handles SIGTERM for a clean shutdown rather than relying on the timeout."---
10Data lost when the container restartedScenario▸
A database or file the app wrote inside the container is simply gone after the container restarted or was recreated — nobody deleted it, it just disappeared.
What is actually happening (the mental model).
A container's writable layer is ephemeral — it exists only as long as that specific container does. Restart, recreate, or reschedule the container (all completely normal operations) and the old writable layer, along with anything written to it, is gone. Containers are meant to be treated as disposable "cattle," not long-lived "pets" with state living inside them.
How to work through it.
- Identify anything that needs to survive the container's lifetime — data, uploads, a database's files.
- Move it to a Docker volume (or a bind mount) so it lives outside the container's writable layer and persists across recreation.
- For anything that must survive the host too (not just the container), use external, durable storage — a managed database, object storage, a network filesystem — rather than any local Docker storage.
The root causes and trade-offs.
Writing state directly into the container's own filesystem instead of a volume. The fix (mount a volume) costs nothing meaningful and should be the default for anything stateful.
The trap that less-experienced engineers fall into.
Treating a container like a small persistent server and being surprised when routine operations (a redeploy, a reschedule, a crash-restart) wipe out data that was never actually persisted outside the container.
🎯 Interviewer follow-up questions you should expect.
- "Why did our data disappear after a restart?" Because it was written to the container's own writable layer, which is ephemeral — it needed to be on a volume instead.
- "How do you persist data across container restarts?" With Docker volumes, or bind mounts, for container-lifetime persistence, and external durable storage if the data must outlive the host too.
---
11Container resource limits / noisy neighbourScenario▸
One container on a host starts consuming most of the CPU or memory, and every other container on the same host slows down or gets OOM-killed — with no limits set, Docker lets any container use as much as the host has.
What is actually happening (the mental model).
Without explicit resource limits, a container can consume the entire host's CPU and memory, starving its neighbours — the classic "noisy neighbour" problem. Worse, a runtime that isn't cgroup-aware (an older JVM or Node process, for example) may look at the host's total memory/CPU to size its own heap or thread pool, not the much smaller limit actually granted to its container — and get OOM-killed even though it "thinks" it has room.
How to work through it.
- Set explicit CPU and memory limits (
--memory,--cpus, or their Kubernetes equivalents) on every container so one workload can't starve others. - Make sure the runtime is cgroup-aware — modern JVM/Node versions read the container's cgroup limits automatically; older ones may need explicit flags (e.g.
-Xmxsized to the container limit, not the host). - Set requests as well as limits (in Kubernetes) so the scheduler places workloads sanely rather than over-packing a host.
The root causes and trade-offs.
No resource limits at all (the default), or limits set but the runtime inside isn't cgroup-aware and mis-sizes itself anyway. The trade-off of setting limits is occasionally OOM-killing your own container if the limit is set too tight — which is a tuning problem, not a reason to skip limits.
The trap that less-experienced engineers fall into.
Setting a memory limit on the container but leaving the JVM/Node heap sizing on its old defaults, which size against the host's memory — so the app OOMs well before it appears to be near the container's own limit.
🎯 Interviewer follow-up questions you should expect.
- "One container is starving the others on a host — what's missing?" Resource limits — you need to add CPU and memory limits per container.
- "We set a memory limit but the app still OOMs early — why?" Because the runtime isn't cgroup-aware, and it's sizing itself against the host's memory rather than the container's limit.
---
12Networking between containers isn't workingScenario▸
A frontend container can't reach a backend container that's supposedly "right there" — often because the code tries to reach it on localhost.
What is actually happening (the mental model).
localhost inside a container refers to the container itself, never the host and never a sibling container, no matter how "close" they seem. Two containers only reach each other by name if they're on the same user-defined network, where Docker's embedded DNS resolves container names to their IPs.
How to work through it.
- Put the containers on the same user-defined bridge network (not just the default bridge, which doesn't do automatic name resolution the same way).
- Have the frontend reach the backend by container/service name, not
localhostand not a hardcoded IP (container IPs change on recreation). - Verify with
docker network inspectand a quickdocker execping/curl by name from one container to the other.
The root causes and trade-offs.
Using localhost or a hardcoded IP where a container/service name was needed, or the two containers simply aren't on the same network. The fix is just correct network configuration — no real trade-off.
The trap that less-experienced engineers fall into.
Reaching for localhost out of habit from running processes directly on a machine — forgetting that each container has its own network namespace and its own idea of what localhost means.
🎯 Interviewer follow-up questions you should expect.
- "Frontend can't reach backend — what's the first thing you check?" Whether the code is using
localhostinstead of the backend's container or service name, and whether they're on the same user-defined network. - "Why not just use the container's IP?" Because container IPs change whenever the container is recreated, while names resolved through Docker's embedded DNS stay stable.
localhost, which inside a container only ever means that container itself. I put containers that need to talk on the same user-defined network and have them address each other by container or service name, which Docker's embedded DNS resolves — never a hardcoded IP, since that changes the moment a container is recreated."---
13Debugging a running container with no shellScenario▸
You need to inspect what's happening inside a running container, but it's a distroless image with no shell — docker exec ... sh just fails.
What is actually happening (the mental model).
Distroless images deliberately have no shell, no package manager, and almost no tooling — that's the whole point, security-wise. So the usual "exec in and poke around" debugging workflow doesn't exist for these images by design, and you need a different approach that doesn't undo the security benefit permanently.
How to work through it.
- Use
docker debug(orkubectl debugwith an ephemeral container in Kubernetes) to attach a separate debugging toolbox to the running container's namespaces, without modifying the production image itself. - Lean more heavily on logs and metrics emitted by the app itself — structured logging and observability reduce how often you need to get inside a container at all.
- If you must, keep a separate debug build with a shell for local troubleshooting — but never ship that build to production.
The root causes and trade-offs.
The lack of a shell is intentional (security), not an oversight — the trade-off you're managing is security vs debuggability, and the answer is external debug tooling, not adding a shell back into the production image.
The trap that less-experienced engineers fall into.
"Fixing" the problem by adding a shell and debugging tools back into the production image — quietly reversing the entire security benefit of going distroless in the first place.
🎯 Interviewer follow-up questions you should expect.
- "How do you debug a distroless container with no shell?" With
docker debugor an ephemeral debug container that attaches without modifying the image, and you invest in logs and metrics so you need this less often. - "Why not just add a shell for convenience?" Because that undoes the entire security reason for going distroless in the first place.
docker debug or kubectl debug, which attach a toolbox to the running container's namespaces without touching the production image. And I try to reduce how often I need to do that at all by investing in good structured logging and metrics up front."---
14Host disk fills up from DockerScenario▸
A host running Docker for a while gradually runs out of disk space, even though nobody's actively creating huge files — builds and deploys eventually start failing with "no space left on device."
What is actually happening (the mental model).
Docker accumulates artifacts over time that are easy to forget about: old/unused images, stopped containers, dangling (untagged) layers from repeated builds, unused volumes, and build cache. None of it gets removed automatically by default, so a host that builds images regularly just keeps growing.
How to work through it.
- Diagnose first —
docker system dfshows exactly where the space is going (images, containers, volumes, or build cache). - Clean up routinely —
docker system prune(and--volumesif unused volumes are safe to remove) rather than only reactively when the disk is already full. - Prevent recurrence — set up a scheduled prune/cleanup job on build hosts, and avoid tagging every build uniquely forever without a retention policy in your registry too.
The root causes and trade-offs.
No routine cleanup policy for images, containers, volumes, and build cache. The trade-off with aggressive pruning is losing cached layers you might have reused — balance cleanup frequency against how much it slows the next build.
The trap that less-experienced engineers fall into.
Only discovering the problem when a build fails from a full disk, then pruning reactively — instead of monitoring disk usage and cleaning up on a schedule before it becomes an incident.
🎯 Interviewer follow-up questions you should expect.
- "A build host slowly fills up its disk — what's accumulating?" Old images, stopped containers, dangling layers, unused volumes, and build cache.
- "How do you find out where the space went?" With
docker system df, followed by a targeted or fulldocker system prune.
docker system df to see where the space actually went rather than guessing, then prune. But the real fix is making cleanup routine and scheduled on build hosts, so it's never an emergency in the first place."---
15Docker Compose in production — is it appropriate?Concept▸
A team is running their production application with a single docker compose up on one VM. Is that a reasonable production setup, or a problem waiting to happen?
What is actually happening (the mental model).
Compose is excellent for local dev and simple single-host setups, but it lacks self-healing, rolling updates, multi-host scheduling, and HA. A single-VM Compose production is a single point of failure — the box dies, the service dies. Whether the answer is Kubernetes, ECS, or something lighter depends on scale and team maturity; a senior names the reliability gap and right-sizes the orchestration to the actual availability requirement (not reflexively pushing everyone to K8s).
How to reason about it.
- Compose is great for local dev and low-stakes single-host services.
- Its gaps for prod: no self-healing (a crashed container stays down), no rolling deploys, no multi-host scheduling, no HA — a single-VM Compose prod is a SPOF.
- Right-size the orchestration: Kubernetes/ECS/Nomad (or Swarm for simple needs) — matched to scale and team maturity. Don't cargo-cult K8s (it's its own operational tax), but do name the reliability gap.
The root causes and trade-offs.
Running Compose on a single VM is simple, but it gives you no high availability and no self-healing if that VM goes down. A real orchestrator is resilient, but it comes with genuine operational overhead to run. The right choice is whichever one actually matches the availability requirement for that workload.
The trap that less-experienced engineers fall into.
Either running critical prod on single-VM Compose (a SPOF) or reflexively insisting everything needs Kubernetes (over-engineering for a team that can't operate it).
🎯 Interviewer follow-up questions you should expect.
- "Is Docker Compose OK for production?" It's fine for a low-stakes single host, but it has no self-healing, rolling updates, or high availability — a single-VM production setup is a single point of failure.
- "When would you move to an orchestrator?" When you need high availability, self-healing, rolling deploys, or multi-host scale, right-sizing the choice, whether Kubernetes, ECS, or Swarm, to the actual requirement and team.
compose up prod has no self-healing, no rolling deploys, and is a single point of failure — the box dies, the service dies. Whether the answer is Kubernetes, ECS, or something lighter depends on scale and team maturity; I wouldn't reflexively push everyone to K8s, that's its own operational tax. But I'd honestly name the reliability gap and right-size the orchestration to the actual availability requirement."---
- Drill the principles first (a container is its PID 1; layers are immutable/permanent; containers are ephemeral;
localhost= the container; minimal = secure; runtimes must be cgroup-aware) — nearly every question is an application of one. - Build and break, in a real setup. Ship a bloated image and shrink it with multi-stage; order layers wrong and watch the cache bust; bake a fake secret and recover it from
docker history; run an app backgrounded and watch the container exit; break inter-container networking by usinglocalhost; set a tiny memory limit and force an OOM. Doing beats reading. - One scenario, three passes. Reason it out, then say the spoken version without looking, then explain it to a teammate — especially PID 1, layers-are-permanent, and the localhost/networking story, which come up constantly.
- Keep a "gotcha → what it actually was" log — PID-1-exited, cache-busting-layer-order, deleted-secret-still-in-layer, localhost-is-the-container. These become the war stories you tell in the behavioural round.
Next in the course order: Kubernetes, at this same depth.