The questions Docker interviews actually ask, answered the way you would say them out loud — short enough to be an answer rather than a lecture, with the follow-up they are usually fishing for.
What is a container, and how is it different from a VM?
A container is a process running on the host's kernel with an isolated view of the filesystem, network and process table. A VM boots its own kernel and a full operating system on top of a hypervisor.
So a container starts in milliseconds and costs what your app costs; a VM starts in tens of seconds and costs a gigabyte before your app does anything. The trade is isolation — a VM has a hypervisor boundary, a container has kernel features, and a kernel bug is a shared problem.
The follow-up: when would you still want a VM? Running untrusted code from strangers, or where a compliance regime demands hardware-level separation.
Image versus container?
An image is a stack of read-only layers on disk, inert. A container is one running instance of an image with a thin writable layer on top.
The consequence worth stating: anything written inside a container goes to that writable layer and disappears when the container is removed. Ten containers from one image share the image's bytes and each get their own scratch space.
CMD versus ENTRYPOINT?
ENTRYPOINT is the executable; CMD is its default arguments. Anything
after the image name on docker run replaces CMD and is appended to
ENTRYPOINT.
So ENTRYPOINT when the image is one program, CMD alone when the
image is an environment somebody might want a shell in.
The follow-up, and the real question: always use exec form, the JSON array. Shell form
runs your process as a child of /bin/sh, which does not forward signals — so
docker stop never reaches your application, it gets SIGKILLed ten seconds later, and
every graceful-shutdown hook you wrote is skipped. Exec form makes your process PID 1, which
receives the signal.
COPY versus ADD?
COPY copies files. ADD also fetches URLs and auto-extracts tarballs.
Use COPY. The auto-extraction is a surprise — ADD app.tar.gz /app
unpacks it, so if you wanted the file you now have its contents. Use
ADD --checksum= only when you specifically want a verified remote fetch.
How does layer caching work?
Each instruction produces a layer with a cache key. For COPY it is a checksum of the
file contents; for RUN it is the literal command string. Docker walks top to bottom
reusing layers — and on the first miss it rebuilds that instruction and everything after
it.
Which gives you the one rule: copy the dependency manifest and install dependencies before
copying your source. On the API in this track that is 8 seconds for a code change versus 92 for a
pom.xml change.
The follow-up: RUN apt-get update on its own line is cached by its command
string, so it can hand you a months-stale package index. Chain it to the install.
Why is my image 1.2 GB?
Almost always: the base image is a full JDK or a full Node image, and the build tooling shipped along with the application.
The fix in order of effect — a multi-stage build, so the compiler and
node_modules never reach the result; then a runtime base rather than a toolchain, JRE
not JDK; then not committing package-manager caches.
Real numbers from this track: the API's build stage is 957 MB and the image that ships is 460 MB. Of that 460, the application itself is 336 kB.
The follow-up they want: a later RUN rm does not shrink an
image. The file is still in the layer that added it; the delete just adds a marker hiding it.
How do you keep a secret out of an image?
Don't put it in. An ARG is printed by docker history, an
ENV is in the image forever, and a COPYed file is recoverable from the
layer even after a RUN rm — docker save and tar, two standard
commands, no exploit.
Needed at build time: RUN --mount=type=secret, which is in no layer. Needed at run
time — the common case: an environment variable at minimum, Docker secrets: to get it
out of docker inspect, a real secret store if you want rotation. And
.dockerignore so .env and .git/ never reach the builder at
all.
Two containers, how do they talk?
By service name on a shared user-defined network. Compose creates one per project and runs DNS on
it, so http://api:8085 resolves.
The port in that URL is the port inside the target container. Publishing with
-p is only for traffic from outside Docker.
The follow-up: why does 127.0.0.1:3309 not work from inside a container?
Because localhost in there means that container, and 3309 is a host-side number that
means nothing on the container network.
depends_on guarantees what?
That the dependency's container was started. Not that the service inside it is ready — so the app races the database and dies on a connection refused.
The fix is a healthcheck on the dependency plus
depends_on: {mysql: {condition: service_healthy}}.
The follow-up: does that mean the app no longer needs retry logic? No.
depends_on only applies to start-up; it says nothing about the database restarting at
three in the morning.
Named volume or bind mount?
Named volume when only the container needs the data — databases, uploads. Bind mount when you need it too — source you are editing, a config file to tweak.
The follow-up worth knowing: a named volume mounted over a non-empty directory
copies the image's contents and ownership on first use. A bind mount does not — it presents the host
directory as-is, which is why the same two lines of YAML give you a working uploads directory one
way and Permission denied the other.
What does :latest actually mean?
Nothing. It is the default tag, and a tag is a mutable pointer that anyone can move.
Deploy it and two servers pulling a minute apart can run different code, nobody can answer "what
is in production", and there is nothing to roll back to. Deploy an immutable tag —
sha-a3f9c21 — and keep :latest for humans typing docker run.
A digest is the only thing that pins bytes.
exec format error?
Architecture mismatch. You built arm64 on an Apple Silicon Mac and the server is
amd64.
docker buildx build --platform linux/amd64,linux/arm64 --push produces a manifest
list so the registry hands each host the right image. Note --push and not
--load: your local store cannot hold two images under one tag.
And you cannot catch this by running it locally — Docker Desktop emulates, so it works on your machine and fails on the server.
Is Docker enough for production?
The one where a confident yes or no is the wrong answer.
For a single host: yes, for plenty of real workloads, once you have set a memory limit, log rotation, a restart policy, a non-root user and a graceful shutdown whose timeouts agree.
What you do not get is anything spanning machines — no failover, no zero-downtime deploy
(compose up stops the old container before starting the new one), no rescheduling off a
dead host, no restart of a container that is unhealthy but still running.
So: a staging environment, an internal tool, a side project — one host is genuinely fine and far simpler to operate. When downtime during a deploy stops being acceptable, that is the requirement that buys an orchestrator, and not before.
Back to the start of the track.