4.5.11 · D5Software Engineering

Question bank — Docker — images, containers, Dockerfile, docker-compose

2,272 words10 min readBack to topic

A quick refresher on the four words you will keep meeting (each is spelled out fully in the parent note):

  • An image is the read-only recipe (stacked immutable layers).
  • A container is a running instance of that image, plus one thin writable layer on top.
  • A Dockerfile is the build script that produces the image.
  • docker-compose is the YAML that runs many containers together.

Before the questions, four small pictures anchor the ideas the traps are built on. Refer back to them as you drill.

Figure — Docker — images, containers, Dockerfile, docker-compose

The layer-caching figure above shows why a bad instruction order is so costly: a red "edit" in one layer poisons every layer stacked below it.

Figure — Docker — images, containers, Dockerfile, docker-compose

The port-mapping figure shows the -p HOST:CONTAINER direction — traffic enters the host door on the left and is forwarded to the container room on the right.

Figure — Docker — images, containers, Dockerfile, docker-compose

The container vs VM figure shows why one is heavy and one is light: the VM stacks a whole guest OS on a hypervisor, the container shares the host kernel.

Figure — Docker — images, containers, Dockerfile, docker-compose

The volume / lifecycle figure shows the ephemeral writable layer dying with the container while a named volume survives outside it.


True or false — justify

A container is basically a lightweight virtual machine
False. A VM boots a whole guest OS on a hypervisor (see figure 3), while a container is just an isolated host process sharing the host kernel via namespaces and cgroups — namespaces hide other processes/mounts/networks so the process thinks it is alone, cgroups cap its CPU and memory.
Two containers from the same image each get their own private copy of every layer
False. The read-only image layers are shared on disk through a union filesystem; Docker only adds a tiny writable top layer per container, so ten containers cost roughly one image plus ten small deltas, not ten full copies.
Deleting a container also deletes the image it came from
False. The image is a separate immutable object with its own reference count; removing the container just discards its writable layer, and you can still docker run a fresh identical instance from the untouched image.
Data written inside a running container survives after you docker rm it
False. Writes land in the ephemeral writable layer, which is destroyed with the container (see figure 4); only a volume or bind mount, which lives outside that layer, keeps data alive.
EXPOSE 8000 in a Dockerfile makes the app reachable from your browser
False. EXPOSE only records metadata ("this app intends to use 8000") for humans and tools; it opens no host port, so you still need -p 8000:8000 (or a ports: entry) at run time to actually publish it.
Every instruction in a Dockerfile creates a new filesystem layer
False. Only instructions that change the filesystem — RUN, COPY, ADD — create real layers; metadata-only instructions like CMD, ENTRYPOINT, EXPOSE, ENV, LABEL, and WORKDIR add image config but no filesystem layer, so they are essentially free and never trigger a re-install cascade.
docker build and docker run do the same job at different times
False. build reads a Dockerfile and produces an image (a static template); run takes an existing image and produces a live container (a process) — two distinct stages, and you can run the same image many times without ever building again.
An image with tag nginx:1.25 and one with nginx:latest are always different images
False. Tags are just human-friendly labels pointing at a content digest; latest can point at the same digest as 1.25, so both names may be two labels on one identical image.
Containers on the same Compose network can talk to each other using the service name as a hostname
True. Compose creates a private network and runs an embedded DNS server that resolves each service name to the container's current IP, so postgres://db:5432 works even though IPs change on restart.
RUN and CMD both execute when the container starts
False. RUN runs once at build time and bakes its result into an image layer; CMD runs at container start time as the main (PID 1) process — build-time versus run-time is the whole distinction.

Spot the error

COPY . . then RUN pip install -r requirements.txt — what's wrong with this order?
Editing any source file changes the COPY layer's content hash; since a layer's cache key includes every layer above it, the install layer below is now considered stale and re-runs on every build. Copying only requirements.txt first means the install layer's inputs change only when dependencies change, so day-to-day code edits stay cached.
docker run -p 8000:8080 myapp when the app listens on port 8000 inside — why does the browser fail?
-p is HOST:CONTAINER (figure 2), so this forwards host-8000 to container-8080, but the app is listening on container-8000 — the forwarded traffic arrives at a port where nothing is bound and the connection is refused. It should be -p 8000:8000.
A Postgres service in Compose with no volumes: entry — what breaks on docker compose down
Postgres writes its database files into the container's writable layer, and down deletes the containers, taking that layer — and all the data — with them. Mounting a named volume at /var/lib/postgresql/data stores the files outside the container so they survive teardown and re-creation.
ENTRYPOINT ["python"] with CMD ["app.py"], then a user runs docker run img script.py — what happens?
ENTRYPOINT is the fixed executable and stays; CMD supplies default arguments that any trailing docker run arguments completely replace, so app.py is dropped and the container runs python script.py. This override-the-args-not-the-program behaviour is exactly why the pair is used.
A Dockerfile ends with RUN python app.py instead of CMD — why won't the container stay up as a server?
RUN executes during the build and its process must finish before the next instruction, so the server would run (and be killed) at build time, leaving nothing to start at run time. With no main process, the container has nothing to keep alive and exits immediately — CMD ["python","app.py"] defers the launch to container start.
Two services in Compose both map "8080:8000" on the host — why does one fail to start?
A TCP host port is an exclusive OS resource: once one container binds host-8080, the kernel refuses a second bind to the same port, so the second container aborts with an "address already in use" error. Give each a distinct host port (e.g. 8081:8000) — the container side can safely repeat.
FROM ubuntu chosen for a tiny Python API — what's the cost?
A full base ships hundreds of packages you never use, so the image is larger (slower to pull and push, more disk) and exposes a wider attack surface (every extra binary is a potential vulnerability). A -slim or Alpine base strips that down to near the bare runtime.

Why questions

Why does Docker boot in milliseconds while a VM takes seconds or minutes?
A VM must boot an entire guest OS — kernel init, drivers, services — on top of a hypervisor before your app runs (figure 3). A container skips all of that: it reuses the already-running host kernel and just starts your process inside fresh namespaces, so "boot" is really just fork/exec plus isolation setup.
Why should the Dockerfile be ordered from least-changing to most-changing instructions?
A layer's cache is valid only if its inputs and every layer beneath it are unchanged (figure 1). Placing rarely-changing steps (base image, dependency install) at the bottom keeps them cached across builds, while volatile steps (your source code) sit on top where their frequent changes invalidate nothing important.
Why can ten containers from one image cost far less disk than ten separate copies?
The read-only image layers are stored once and mounted into every container through a union filesystem; each container adds only a thin copy-on-write writable layer, so the shared bulk is never duplicated — you pay for one image plus ten small deltas.
Why use ENTRYPOINT + CMD together instead of one CMD?
ENTRYPOINT pins the program that always runs, while CMD supplies default arguments the user can override on docker run without retyping the executable. This gives the image a clean command-line interface — like a compiled tool with sensible defaults — instead of forcing users to know and repeat the full command.
Why does Compose need depends_on for a web-plus-db stack?
Without it, Compose may start web before db, and the web app's first connection attempt hits a database that is not yet accepting connections. depends_on fixes the start order of containers (though note it waits for the container to start, not for Postgres to be fully ready — a health check handles that).
Why pass config like DATABASE_URL as an environment variable rather than hard-coding it in the image?
The 12-factor principle says config that varies between deployments must live outside the build. Baking the URL into the image would force a rebuild for every environment; injecting it as an env var lets one immutable image run unchanged in dev, staging, and prod.
Why is docker exec -it <id> bash useful even though the image already has a CMD?
CMD launched the container's main process; exec spawns an additional process (an interactive shell) inside the same namespaces, so you land in the running container's filesystem and network to inspect logs, files, or processes — all without disturbing or restarting the main app.

Edge cases

What runs if a container's image has no CMD and no ENTRYPOINT at any layer?
There is no start command, so docker run fails with an error like "no command specified". In practice most images do inherit a default from their base — e.g. python:3.12 sets CMD ["python3"] and postgres:16 sets an ENTRYPOINT that launches the DB — so you only hit this error when you build FROM scratch (an empty base) or override the defaults away.
What happens to a -d (detached) container whose main process exits immediately?
The container stops the instant its main process ends, because a container lives exactly as long as its PID 1 process. A one-shot script or a server that crashes on startup therefore shows up as "Exited" the moment you docker ps -a — the fix is to keep a long-running process as PID 1.
If you docker build twice with zero file changes, what does the second build do?
It matches each instruction against the build cache, finds every input identical, and reuses the existing layers, so the image is produced almost instantly with "CACHED" printed beside each step — no re-download, re-install, or re-copy occurs.
What is a bind mount ./code:/app good for that a named volume is not?
A bind mount maps a specific host directory straight into the container, so edits you make on the host appear instantly inside — ideal for live-reload development. A named volume is Docker-managed opaque storage meant for persistence (like a database), not for editing files with your host editor.
Can two apps needing different versions of the same system library coexist on one host with Docker?
Yes. Each app ships in its own image with its own copy of that library, and containers do not share userspace libraries, so version 1 and version 2 run side by side with zero conflict — this dependency-isolation is the exact problem Docker was created to solve.
What survives if you delete a container but the image is still tagged?
Everything except that one container's writable layer: the image, all its read-only layers, and any named volumes remain intact, so you can immediately docker run a brand-new identical container from the same image — the recipe is untouched.

Recall One-sentence self-test

Cover every answer above and re-derive the reasoning; if you can only say "true/false" without the why, you have memorised the trap, not defused it.