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
$ docker image inspect python:3.14-slim
4 layers, 178,175,783 bytes
python@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170Each 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
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? noBoth 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
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
docker rm -f c1
docker run --rm python:3.14-slim test -f /tmp/note.txt -> goneThe 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
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 |
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 RAMNamed 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.
Related
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 topshowed it as an ordinary host process (PID 3017), PID 1 inside its own namespace.
- Follow-up: “Is a container a lightweight VM?” - no: it shares the host
kernel.
- “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 stopwait the whole grace period and then kill my app?” - the app is PID 1 without aSIGTERMhandler, and the kernel gives PID 1 no default handling, so the signal is ignored untilSIGKILL(exit 137). Exec-formCMD, a handler, or--initfixed 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.
- Follow-up: “Shell form vs exec form?” - shell form runs
- “Tag or digest?” - tags are moved by the publisher; a digest names exact bytes. Pin digests for reproducible builds and deploys.