Ch. 13 · Docker

Docker Images Versus Running Containers

An image is a read-only template; a container is that template plus a writable layer and a process. See what survives stop, start and rm.

~5 min readbeginnerupdated Oct 7, 2026

“What’s the difference between an image and a container?” is often the first Docker question in an interview, and the short answer (“a container is a running image”) is the one that earns a follow-up. The useful answer explains where each one lives on disk, what happens to files you change, and which commands act on which object.

Everything below was run against alpine:3.20 on a local Docker Engine, so the IDs and sizes are real but will differ on your machine.

What an image actually contains

An image is two things: an ordered stack of read-only filesystem layers, and a JSON config that says how to run it. The config holds the default command, environment variables, working directory, user, exposed ports and labels. You can look at both:

docker image inspect alpine:3.20 \
  --format 'id={{.Id}} layers={{len .RootFS.Layers}} cmd={{json .Config.Cmd}}'
Terminal
id=sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc layers=1 cmd=["/bin/sh"]
Text

The ID is a SHA-256 digest of the content, which is why an image can’t change. If anything in it changed, it would have a different ID and be a different image. Tags like 3.20 are just movable names pointing at an ID (the tags note covers what that means for releases).

Nothing is running when an image exists. It’s a file on disk, the same way a compiled binary is a file before you execute it. That comparison is closer than the common “class and object” analogy, because a binary, like an image, can’t remember what happened in previous runs.

What a container adds on top

docker run (or docker create followed by docker start) builds a container from an image. Docker:

  • stacks a new, empty, writable layer over the image’s read-only layers, using a union filesystem (overlayfs on most Linux hosts),
  • creates namespaces so the process gets its own PID tree, network stack, hostname and mount table,
  • applies cgroup limits for CPU and memory if you set any,
  • records the container’s own settings (name, port mappings, mounts, environment overrides, restart policy),
  • starts the process from the image config, or whatever you overrode on the command line.

Start two containers from the same image and they share every read-only layer but get separate writable layers:

docker run -d --name a alpine:3.20 sleep 600
docker run -d --name b alpine:3.20 sleep 600
docker exec a sh -c 'echo hello > /tmp/note.txt; rm /etc/motd; echo x >> /etc/hosts.allow'
docker exec b ls /tmp      # prints nothing: b never sees a's file
Terminal

docker diff lists what a container has changed relative to its image. A is added, C changed, D deleted:

$ docker diff a
C /etc
A /etc/hosts.allow
D /etc/motd
C /tmp
A /tmp/note.txt
Text

Deleting /etc/motd didn’t remove anything from the image. The writable layer records a “whiteout” that hides the file from this container only. Container b, and every future container, still has it. Modifying an existing file works the same way: the first write copies the whole file up into the writable layer (copy-on-write), and later reads see the copy. That’s cheap for config files and expensive for a 2 GB database file, which is one reason databases belong on volumes.

docker ps -s shows the two sizes side by side:

NAMES   SIZE
b       4.1kB (virtual 9.51MB)
a       20.5kB (virtual 9.53MB)
Text

The first number is the writable layer. “Virtual” includes the shared image layers, so adding up virtual sizes across containers wildly overstates disk usage.

Stop, start and remove are not the same thing

A container has a lifecycle independent of its process: created, running, paused, exited, removed. Stopping keeps the writable layer:

$ docker stop a && docker start a
$ docker exec a cat /tmp/note.txt
hello
Text

Removing deletes it, and a new container from the same image starts clean:

$ docker rm -f a
$ docker run --rm alpine:3.20 cat /tmp/note.txt
cat: can't open '/tmp/note.txt': No such file or directory
Text

This is the misconception interviewers are fishing for. People say “containers are stateless” and then get surprised that a stopped container still has its files, or they say “my data is in the container, it’s fine” and then lose it when Compose recreates the service after a config change. Both mistakes come from not distinguishing stop from remove. docker compose up replaces a container whenever its configuration or image changes, so anything you care about must live in a volume or outside the container entirely.

The commands follow the split. docker image ls, docker image rm, docker build, docker pull and docker push act on images. docker ps, docker logs, docker exec, docker stop and docker rm act on containers. You can’t remove an image while a container (even a stopped one) still references it, without forcing it.

Why not just commit the container?

docker commit turns a container’s writable layer into a new image layer. It works, and it’s occasionally handy for capturing a broken state to investigate. As a way to produce images it’s a trap: nobody can see how the image was made, you can’t rebuild it when the base image gets a security fix, and whatever junk was in the container (shell history, caches, a credentials file you copied in for a minute) is baked in. A Dockerfile is the reviewable, repeatable recipe; the image is its output; containers are disposable instances of that output.

Interview tip

Treat a container’s writable layer as scratch space. If you find yourself docker exec-ing in to fix something, the fix belongs in the Dockerfile or the run configuration.

Interview practice

A developer runs a Postgres container with docker run -d --name db postgres:16, loads test data, then changes an environment variable by running docker rm -f db and the same docker run with the new variable. The data is gone. They say Docker “lost” it. Explain what happened, and why the data might not be quite as gone as they think.

Answer

The data was written inside the container, not into the image. Removing the container deleted its writable layer and the new container started from the unchanged image, so from the developer’s point of view the database was empty.

The twist is that the official Postgres image declares VOLUME /var/lib/postgresql/data. When a container starts without a mount for that path, Docker creates an anonymous volume for it, so the table files were actually in that volume rather than the writable layer. docker rm -f db doesn’t remove anonymous volumes unless you add -v, so the old data is probably still sitting in an unnamed volume (docker volume ls shows a long hex name). The new container got a fresh anonymous volume, which is why it looked empty.

The fix is a named volume, -v pgdata:/var/lib/postgresql/data, which the new container reattaches to by name. Container settings like environment variables can’t be changed in place, so recreating containers is normal; the design job is making sure state never depends on a specific container surviving.

Keep going

More in Docker

esc