Backend / Docker / 02_image_vs_container.md

Docker image vs container

Updated 4 interview angles 6 min read source
On this page9
  1. An image: layers, config, digest
  2. A container: a process with a writable layer
  3. Where data can live
  4. Limits the process cannot see
  5. PID 1 and docker stop
  6. Where it breaks in production
  7. The trade not taken
  8. Related
  9. Interview angle

Docker image vs container

Article.

An image is a read-only stack of filesystem layers plus a config (command, environment, user, ports), identified by a content hash. A container is a process on the host, started from that image, with its own namespaces, a cgroup that limits its resources, and a thin writable layer on top of the image’s layers.

“What is a container, really, and what happens to the files it writes?” The answers below come from Docker Engine 29.8 (Docker Desktop on Windows, Linux containers) with python:3.14-slim.

An image: layers, config, digest

text
$ docker image inspect python:3.14-slim
4 layers, 178,175,783 bytes
python@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170

Each layer is a set of filesystem changes, content-addressed by its hash, so identical layers are stored once and shared by every image and container that uses them (Docker image layers and build caching). The image also carries metadata: the default CMD/ENTRYPOINT, environment variables, the user it runs as (root unless the Dockerfile says otherwise), exposed ports.

Tags move, digests do not

python:3.14-slim is a tag: the publisher repoints it with every patch release. python@sha256:a2b8... is a digest: it names exactly these bytes forever. Two builds a week apart from the same FROM python:3.14-slim can get different base images; pinning the digest makes builds reproducible, and an automated updater (Renovate, Dependabot) keeps the pin current.

A container: a process with a writable layer

Writes go to the container’s own layer

text
two containers from one image; c1 writes /tmp/note.txt
docker diff c1:   C /tmp
                  A /tmp/note.txt
docker diff c2:   (nothing)
c2 sees the file? no

Both containers share the image’s read-only layers; each has its own writable layer on top (overlayfs). docker diff lists what changed in it: A added, C changed, D deleted.

Copy-on-write costs the whole file

text
append one byte to a 5,651,216-byte file that came from the image
docker ps -s:   c1  5.69MB (virtual 140MB)
                c2  4.1kB  (virtual 135MB)

Changing one byte of an image file copied the whole file up into the writable layer. Databases, logs, caches, anything that rewrites large files, pays this on every container and loses it when the container goes. That is why such data belongs in a volume (managed by Docker, outside the container’s layers) or a bind mount.

Removed means gone

text
docker rm -f c1
docker run --rm python:3.14-slim test -f /tmp/note.txt   ->  gone

The writable layer dies with the container. A new container from the same image starts from the image again; that is the property that makes containers replaceable, and the reason state must live elsewhere.

It is just a process

text
docker top c1             PID 3017   sleep      (on the host)
inside c1: PID 1 is       sleep
/sys/fs/cgroup/memory.max  max        (no limit set)

There is no virtual machine: the container’s command is an ordinary process on the host kernel. Namespaces give it its own view of process ids (it is PID 1 inside), network, mounts and hostname; a cgroup caps its CPU and memory (--memory, --cpus); here no limit was set. On Docker Desktop the host is a small Linux VM, which is why a Linux container runs on Windows at all.

Where data can live

Storage Lives in Survives docker rm
writable layer the container no
named volume Docker’s volume area yes
bind mount a host directory yes
tmpfs memory no, and never on disk
bash
docker run -v pgdata:/var/lib/postgresql/data postgres:18   # named volume
docker run -v "$PWD/src:/app/src" myapp                      # bind mount, dev only
docker run --tmpfs /tmp myapp                                # scratch space in RAM

Named volumes are for data the container owns (a database); bind mounts for sharing host files, mostly in development; tmpfs for scratch data and secrets that must never touch disk.

Limits the process cannot see

A cgroup limits the container, but most runtimes inside it do not read the cgroup. In a container started with --memory=128m, a Python process saw os.cpu_count() = 12 (every host CPU) and was killed by the kernel with exit 137, with no MemoryError and no traceback, when it crossed the limit (Python memory in production).

Worker counts derived from the CPU count oversubscribe a container limited to two CPUs; size them from the limit explicitly.

PID 1 and docker stop

docker stop sends SIGTERM, waits for the grace period, then sends SIGKILL. The kernel gives PID 1 no default signal handling, so a process that does not install a handler simply ignores SIGTERM:

Container command docker stop Exit code
python app.py as PID 1 waited the full grace period 137 (SIGKILL)
sh -c "python app.py" waited the full grace period 137
docker run --init 0.4 s 143 (SIGTERM)
app handles SIGTERM 0.4 s 0

Every deploy of the first two kills the process mid-request with no cleanup: connections are not drained, in-flight work is lost. Use the exec form (CMD ["python", "app.py"], not the shell form), install a SIGTERM handler (uvicorn and gunicorn do), or run a minimal init (--init, tini) as PID 1, which also reaps zombie processes.

Set Kubernetes’ terminationGracePeriodSeconds above the longest request (What is Celery).

Where it breaks in production

docker commit as a build process

Someone fixes a container by hand and commits it as the new image. Nobody can rebuild it, its layers include whatever else was lying around, and the next deploy from the Dockerfile reverts the fix. Images come from Dockerfiles in CI, always.

Data in the writable layer

A database or upload directory without a volume: everything works until the container is replaced, and then the data is gone. Containers are cattle; their state must be in volumes, managed databases or object storage.

Running as root

The image’s default user is root, so the process is root inside the container, and a container escape is root on the host. Add a USER in the Dockerfile and drop capabilities.

Mutable tags in production

FROM python:3.14-slim and image: myapp:latest mean a rebuild or a node restart can silently run different bytes. Deploy by digest or by immutable version tags, and pin base images.

The trade not taken

A virtual machine gives each workload its own kernel: stronger isolation, a full OS to manage, seconds-to-minutes boot, gigabytes of overhead. A container shares the host kernel: millisecond starts, megabytes of overhead, and isolation only as strong as namespaces, cgroups and the kernel itself. Where tenants do not trust each other, sandboxed runtimes (gVisor) or micro-VMs (Firecracker, used by AWS Lambda and Fargate) sit between the two.

Interview angle 4

  • “Image vs container?” - an image is immutable layers plus config, named by a content digest; a container is a process on the host started from it, isolated by namespaces, limited by cgroups, with its own thin writable layer.
    • Follow-up: “Is a container a lightweight VM?” - no: it shares the host kernel. docker top showed it as an ordinary host process (PID 3017), PID 1 inside its own namespace.
  • “What happens to data written inside a container?” - it goes to the container’s writable layer, invisible to other containers and gone on docker rm. Changing one byte of a 5.6 MB image file copied all 5.6 MB. Persistent data goes in volumes.
  • “Why does docker stop wait the whole grace period and then kill my app?” - the app is PID 1 without a SIGTERM handler, and the kernel gives PID 1 no default handling, so the signal is ignored until SIGKILL (exit 137). Exec-form CMD, a handler, or --init fixed it: 0.4 s, clean exit.
    • Follow-up: “Shell form vs exec form?” - shell form runs sh -c, which becomes PID 1 and does not forward signals; exec form makes your process PID 1.
  • “Tag or digest?” - tags are moved by the publisher; a digest names exact bytes. Pin digests for reproducible builds and deploys.