Docker Images Explained

A Docker image is the read-only template Docker uses to create containers. It contains a filesystem, default command, environment metadata, exposed-port metadata, labels, and links to the layers that make up the image.

Images matter because they are the portable unit you build, share, scan, deploy, and roll back. If you understand images, containers stop feeling mysterious: a container is mostly an image plus a thin writable layer and a running process.

Overview: How Docker Images Work

An image is not one giant archive in normal use. It is described by a manifest and built from layers. Each layer records filesystem changes such as adding files, installing packages, or deleting files. Docker stacks those read-only layers together with a union filesystem so the final container sees one merged root filesystem.

When you run docker run nginx:1.27-alpine, the Docker client sends a request to the Docker daemon. If the image is not already present locally, the daemon asks a registry, usually Docker Hub unless the name points somewhere else, for a manifest. The manifest tells Docker which config object and which compressed layer blobs are needed for your platform, such as Linux on amd64 or arm64. Docker downloads missing blobs, verifies their content digests, unpacks them into its local storage, creates a container writable layer above the image layers, then starts the configured command.

This is why images and containers have different lifecycles. Removing a stopped container with docker rm removes its writable layer and metadata, but leaves the image behind. Removing an image with docker image rm removes local tags and unused layer references, but Docker cannot delete layers still used by a container or another image.

Tags are human-friendly pointers, not permanent identities. nginx:1.27-alpine means repository nginx and tag 1.27-alpine. The same tag can be republished by the image owner. A digest such as nginx@sha256:... identifies exact content. In production, prefer specific tags and, for strict reproducibility, digests from your build pipeline.

Syntax

docker image COMMAND [OPTIONS]

docker pull [OPTIONS] NAME[:TAG|@DIGEST]
docker image ls [OPTIONS]
docker image inspect IMAGE [IMAGE...]
docker image history IMAGE
docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]
docker image rm IMAGE [IMAGE...]
Command Purpose
docker pull Download an image manifest and any missing layers from a registry.
docker image ls List images known to the local Docker daemon.
docker image inspect Show detailed image metadata, including config, architecture, labels, and layer digests.
docker image history Show the image’s build history and approximate layer sizes.
docker tag Add another local name to the same image ID.
docker image rm Remove a local image tag or image if no container still depends on it.

Most image names have the form [registry/][namespace/]repository[:tag]. For example, docker.io/library/nginx:1.27-alpine is the fully expanded form of nginx:1.27-alpine. If you omit the tag, Docker uses latest, which is only a tag name, not a promise that the image is newest or stable.

Examples

Pull and list an image

docker pull nginx:1.27-alpine
docker image ls nginx

Output:

1.27-alpine: Pulling from library/nginx
Digest: sha256:...
Status: Downloaded newer image for nginx:1.27-alpine
docker.io/library/nginx:1.27-alpine

REPOSITORY   TAG           IMAGE ID       CREATED        SIZE
nginx        1.27-alpine   a97e2b4c6f8a   2 weeks ago    47MB

The pull downloads only layers your machine does not already have. The image list shows the repository, tag, local image ID, creation time, and virtual size. The exact ID and size can differ by CPU architecture because a multi-platform tag can resolve to different image manifests.

Inspect metadata and layer history

docker image inspect nginx:1.27-alpine
docker image history nginx:1.27-alpine

Output:

[
  {
    "Id": "sha256:a97e2b4c6f8a...",
    "RepoTags": ["nginx:1.27-alpine"],
    "Architecture": "amd64",
    "Os": "linux",
    "RootFS": {
      "Type": "layers",
      "Layers": ["sha256:...", "sha256:..."]
    }
  }
]

IMAGE          CREATED BY                                      SIZE
nginx:1.27...  CMD ["nginx" "-g" "daemon off;"]               0B
missing        COPY docker-entrypoint.sh /                    4.6kB
missing        RUN /bin/sh -c apk add --no-cache curl          8.1MB

inspect is the truth source for image metadata. It shows platform, config, labels, environment, exposed-port metadata, default command, and the root filesystem layer chain. history is easier to scan when you want to understand why an image is large or which Dockerfile instruction introduced a layer.

Build a tagged application image

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]
docker build -t my-node-api:1.0 .
docker image ls my-node-api
docker run --rm -p 3000:3000 my-node-api:1.0

Output:

[+] Building 8.4s (9/9) FINISHED
 => [internal] load build definition from Dockerfile     0.0s
 => [internal] load metadata for docker.io/library/node:20-alpine
 => [2/5] WORKDIR /app                                  0.0s
 => [3/5] COPY package.json package-lock.json ./         0.1s
 => [4/5] RUN npm ci --omit=dev                         6.7s
 => [5/5] COPY server.js ./                              0.1s
 => exporting to image                                   0.2s
 => naming to docker.io/library/my-node-api:1.0          0.0s

The Dockerfile starts from a pinned base image tag, installs production dependencies before copying the changing application file, records port 3000 as metadata, and sets the default command. EXPOSE does not publish the port to your host. The actual publication happens in docker run -p 3000:3000.

How It Works Step by Step

  1. The Docker CLI reads your command and sends an API request to the Docker daemon.
  2. For docker pull, the daemon contacts the registry, negotiates the platform, fetches a manifest, and downloads missing layer blobs by digest.
  3. For docker build, BuildKit reads the Dockerfile, sends the build context, executes instructions, and stores cache records. Instructions such as RUN, COPY, and ADD usually create filesystem layers; metadata instructions such as CMD, ENV, LABEL, and EXPOSE update the image config.
  4. Docker verifies downloaded content by digest, unpacks filesystem layers, and records image metadata in local storage.
  5. When a container starts, Docker mounts the image layers read-only, adds a writable container layer, applies namespaces and resource settings, then starts the configured process.
  6. When the container writes a file that came from the image, the storage driver uses copy-on-write: the file is copied into the writable layer and changed there. The original image layer remains unchanged.

Build caching follows the same layer idea. Docker can reuse a cached result when an instruction and its inputs are unchanged. Once one layer changes, that layer and every later layer must be rebuilt. That is why dependency manifests should be copied before application source: a change to server.js should not force npm ci to run again.

Common Mistakes

Treating latest as a stable version

FROM node:latest
WORKDIR /app
COPY . .
RUN npm install
ENV API_KEY=changeme
CMD ["node", "server.js"]

This wrong example has three problems. node:latest can change underneath you, COPY . . before dependency installation ruins cache reuse, and the ENV line bakes a realistic-looking secret into an image layer. A later RUN rm would not remove it from earlier layer history. Use a pinned tag such as node:20-alpine, copy dependency files first, and inject secrets at runtime through your platform’s secret store or mounted files.

Confusing images with containers

If docker image rm my-node-api:1.0 fails because a stopped container uses it, remove the container first with docker rm. The image is the template; the container is an instance with its own writable layer. Deleting one does not automatically delete the other.

Expecting EXPOSE to publish a port

EXPOSE 3000 is documentation and image metadata. It helps humans and tools know the intended container port, but it does not open the port on the host. Use docker run -p 3000:3000 image-name or Compose ports: when you need host access.

Best Practices

  • Use specific base image tags, and use digests in pipelines that require exact reproducibility.
  • Keep images small by choosing minimal base images and installing only runtime dependencies.
  • Order Dockerfile instructions from least-changing to most-changing to preserve build cache.
  • Add a .dockerignore file so build contexts do not include node_modules, logs, secrets, test output, or local caches.
  • Never bake secrets into image layers with ENV, ARG, copied files, or build scripts.
  • Use multi-stage builds when an app needs compilers or build tools. Compile in a builder stage, then COPY --from=builder only the finished artifacts into a slim runtime stage.
  • Scan images in your registry or CI system and rebuild regularly so base image security fixes are included.
  • Tag images with meaningful release identifiers such as myapp:1.4.2 or a Git commit SHA, not only latest.

Practice Exercises

  1. Pull redis:7.4-alpine, list local Redis images, and inspect the image architecture. Hint: use docker image inspect and search the JSON for Architecture.
  2. Create a Dockerfile for a small Node app that copies dependency files before source code. Expected end state: changing only server.js should reuse the dependency-install layer.
  3. Build an image tagged with both inventory-api:1.0 and inventory-api:stable. Hint: build once, then add the second name with docker tag.

Summary

  • A Docker image is a read-only template made from metadata plus stacked filesystem layers.
  • A container is created from an image by adding a thin writable layer and starting a process.
  • Registries store manifests, config objects, and layer blobs; tags point to image content but can move.
  • Layer order controls build cache effectiveness, image size, and rebuild speed.
  • EXPOSE documents a port; -p or Compose ports: publishes it.
  • Use pinned tags, small images, .dockerignore, multi-stage builds, and runtime secret injection for production-quality images.