How Docker Works (Images, Containers, Layers)
Docker works by packaging an application into an image and then starting containers from that image. An image is a read-only template made of layers; a container is an isolated process with a thin writable layer on top. Understanding this model explains why Docker is fast, why containers are disposable, and why image tags, ports, volumes, and build cache behavior matter.
Overview: How Docker Works
The easiest way to think about Docker is this: images are saved application filesystems, and containers are running instances of those filesystems. The image contains files, installed packages, environment defaults, exposed-port metadata, labels, and a default command. The container adds runtime choices: its name, networking, mounts, environment variables, resource limits, and current process state.
Images are not usually stored as one giant file. They are stacks of read-only layers. A base image such as alpine:3.20 has layers for a tiny Linux userland. An application image adds more layers for copied files, installed dependencies, and metadata. If two images share a base layer, Docker stores that layer once and reuses it. This is why pulling a related image can be quick after you already have the base image locally.
A container adds one more layer: a thin writable layer. When a process inside the container writes to /tmp/report.txt, Docker stores that change in the container’s writable layer, not in the image. If you delete the container, that writable layer is deleted too. The original image is unchanged and can start another clean container. Persistent data belongs in a volume, bind mount, database service, or external storage.
The Docker CLI and Docker daemon are separate pieces. The docker command you type is a client. It sends API requests to the Docker daemon, often through a Unix socket on Linux or through Docker Desktop’s managed connection on macOS and Windows. On native Linux, containers use kernel features such as namespaces for isolation, cgroups for resource control, and a union filesystem driver for layers. On Docker Desktop, those Linux features run inside a small Linux VM, even though the CLI is used from macOS or Windows.
Registries store images for sharing. A registry does not merely store a tag string like nginx:1.27-alpine. A tag points to a manifest, and the manifest lists the image configuration plus content-addressed layer blobs. When Docker pulls an image, it downloads only missing layers and verifies content by digest. For stronger reproducibility, production systems often test a tag and then deploy a digest-pinned reference.
Syntax
The main command shapes for this lesson are:
docker pull IMAGE[:TAG]
docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARGUMENTS]
docker image ls
docker container ls --all
docker inspect NAME_OR_ID
| Part | Meaning |
|---|---|
IMAGE[:TAG] |
The image name and optional tag. If omitted, Docker commonly uses latest, which is a moving tag and should not be used for reliable production deployments. |
docker pull |
Downloads image metadata and missing layers from a registry without starting a container. |
docker run |
Creates a container from an image and starts its configured command, or a command you provide. |
--name web |
Gives the container a stable name for later docker logs, docker stop, and docker rm. |
--rm |
Removes the container automatically when it exits. The image remains. |
-d |
Runs the container in detached mode, leaving it in the background. |
-p 8080:80 |
Publishes host port 8080 to container port 80. This is what makes a container service reachable from the host. |
-v name:/path |
Mounts a named volume at a path inside the container so data can outlive the container. |
docker inspect |
Prints detailed JSON metadata about an image or container, including layer information, mounts, network settings, and the configured command. |
Examples
Example 1: Pull an Image and See the Layers
docker pull alpine:3.20
docker image ls alpine:3.20
Output:
3.20: Pulling from library/alpine
Digest: sha256:beef1234example
Status: Downloaded newer image for alpine:3.20
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine 3.20 1d34ffeaf190 4 weeks ago 7.8MB
The pull asks a registry for the manifest behind alpine:3.20, downloads missing layer blobs, and stores them locally. The image row is a convenient name and tag pointing to local content. The exact image ID, digest, age, and size can differ on your machine because image publishers release updates and Docker may choose a platform-specific image.
Example 2: Run a Container, Then Remove Only the Container
docker run --name layer-demo alpine:3.20 sh -c "echo saved-in-container > /note.txt && cat /note.txt"
docker container ls --all --filter name=layer-demo
docker rm layer-demo
docker image ls alpine:3.20
Output:
saved-in-container
CONTAINER ID IMAGE COMMAND STATUS NAMES
8d4c7d2f1a92 alpine:3.20 "sh -c 'echo saved...'" Exited (0) 5 seconds ago layer-demo
layer-demo
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine 3.20 1d34ffeaf190 4 weeks ago 7.8MB
The file /note.txt existed in the container’s writable layer. Removing the stopped container removed that writable layer, but the image stayed installed. Starting a fresh container from alpine:3.20 will not contain /note.txt unless the image itself includes it or you mount persistent storage.
Example 3: Build an Image with Reusable Layers
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]
Output:
Dockerfile instructions create an image where dependency installation can be cached separately from application source changes.
This Dockerfile uses a pinned base image tag instead of node:latest. The dependency files are copied before server.js, so changing only application code does not normally invalidate the npm ci layer. The EXPOSE 3000 instruction is documentation and image metadata only; it does not publish a host port. You still need -p 3000:3000 or a Compose ports: entry when running the container.
docker build -t layer-api:1.0 .
docker run --rm -p 3000:3000 layer-api:1.0
Output:
[+] Building 6.4s (9/9) FINISHED
Successfully tagged layer-api:1.0
Server listening on port 3000
The build creates a new image named layer-api:1.0. The run command starts a container from that image and publishes container port 3000 on the host. If the main Node process exits, the container stops because a container’s lifetime is tied to its main process.
How It Works Step by Step
- You type a Docker command. The CLI parses it and sends a request to the Docker daemon.
- For
docker run, the daemon checks whether the requested image exists locally. If not, it resolves the tag through a registry manifest and downloads missing layers. - Docker creates a container record containing the image reference, command, environment variables, mounts, network settings, labels, and restart policy.
- The storage driver mounts the image’s read-only layers as a single filesystem view and places a writable container layer on top.
- Docker sets up isolation. The process receives its own filesystem view, process namespace, hostname, network interface, and resource accounting. On Docker Desktop this happens inside the managed Linux VM.
- If you used
-p, Docker configures host-to-container port forwarding. If you only usedEXPOSEin the image, no host port is opened. - The daemon starts the configured command. Output written to stdout and stderr is captured and available through
docker logs. - When the main process exits, the container becomes stopped. Its writable layer remains until
docker rm, unless the container was created with--rm.
Builds follow the same layer idea. Each Dockerfile instruction produces a cacheable result. Changing an instruction, or files used by a COPY instruction, invalidates that layer and every layer after it. This is why good Dockerfiles put slow, stable work such as dependency installation before frequently changing source-code copies.
Common Mistakes
Confusing Images and Containers
docker rm nginx:1.27-alpine
This is wrong because docker rm removes containers, not images. Use docker rm container-name for a stopped container and docker image rm image:tag for an image. If a container still references the image, remove the container first or Docker will refuse to delete the image.
Depending on latest
docker run -d --name web nginx:latest
The latest tag is just a tag, not a guarantee that you are using the newest safe version or the same version as yesterday. For repeatable work, choose a specific tag after testing:
docker run -d --name web nginx:1.27-alpine
Expecting EXPOSE to Publish a Port
FROM nginx:1.27-alpine
EXPOSE 80
This image documents that Nginx listens on port 80, but it does not bind host port 80. Publish a port when creating the container:
docker run --rm -p 8080:80 nginx:1.27-alpine
Losing Data in the Writable Layer
docker run --name redis-temp redis:7.2-alpine
docker rm redis-temp
Any data written only inside that container is gone when the container is removed. Use a named volume for persistent service data:
docker volume create redis-data
docker run -d --name redis-db -v redis-data:/data redis:7.2-alpine
Baking Secrets into Layers
FROM alpine:3.20
ENV API_KEY=changeme
RUN echo "$API_KEY" > /tmp/key.txt
RUN rm /tmp/key.txt
This is still unsafe. The secret value may remain in image history or in an earlier layer even if a later instruction removes a file. Supply secrets at runtime through Docker secrets, mounted files, environment supplied by your orchestrator, or a secret manager.
Best Practices
- Use precise image tags such as
nginx:1.27-alpine,redis:7.2-alpine, ornode:20-alpineinstead of relying onlatest. - Treat containers as disposable. Put important state in volumes, bind mounts, databases, or external services.
- Remember that
EXPOSEis metadata. Usedocker run -por Composeports:to publish ports. - Name long-lived containers so operational commands are predictable.
- Keep Dockerfiles cache-friendly: copy dependency manifests first, install dependencies, then copy changing source files.
- Add a
.dockerignorefile so build contexts do not include Git history, dependency folders, logs, caches, or local secrets. - Do not bake secrets into images. Layers are historical content, and deleting a secret later in the Dockerfile does not reliably erase it.
- Use
docker inspect,docker logs,docker image ls, anddocker container ls --allto verify what Docker actually created. - Prefer
docker composefor multi-container applications. The olddocker-composecommand is legacy.
Practice Exercises
- Pull
nginx:1.27-alpine, list it withdocker image ls, then run it with the namepractice-weband publish host port8090to container port80. Expected end state:http://localhost:8090reaches Nginx. - Run an
alpine:3.20container that writes a file inside the container, remove the container, and start a new one from the same image. Expected lesson: the file was in the removed container’s writable layer, not in the image. - Create a named volume for
redis:7.2-alpineand mount it at/data. Hint: the volume should survive after the container is stopped and removed.
Summary
- An image is a read-only template made from stacked layers and image configuration.
- A container is an instance of an image with runtime settings, a main process, and a thin writable layer.
- The Docker CLI talks to the daemon; the daemon pulls layers, creates filesystems, configures isolation, and starts processes.
- Registries store manifests and content-addressed layer blobs behind image names and tags.
- Changing a build layer invalidates that layer and all later layers, so Dockerfile order affects build speed.
- Removing a container does not remove the image, and removing an image does not remove a running container.
- Persistent data belongs outside the container writable layer, usually in a named volume or external service.
