Docker Image Layers Explained
Understand Docker image layers, how Dockerfiles create them, how layers are cached and reused, and how to optimize Docker images.
Docker images are not stored as one large, indivisible file. They are built from a series of layers. Each layer represents a filesystem change, and Docker combines these layers to produce the filesystem that a container sees when it starts. Understanding image layers is important for building smaller images, improving build speed, making better use of Docker's cache, and avoiding accidental inclusion of unnecessary files.
Layers are closely connected to Dockerfiles. Instructions such as RUN, COPY, and ADD can create filesystem changes that become part of an image's history. When a Docker image is rebuilt, Docker can reuse previously built layers when the relevant inputs have not changed. This is one of the main reasons that the order of Dockerfile instructions matters.
What Is a Docker Image Layer?
A Docker image layer is a filesystem change stored as part of an image. Instead of creating a completely new copy of the entire filesystem for every Dockerfile instruction, Docker represents the image as a stack of layers. The final container filesystem is assembled from those layers.
A layer can add files, modify files, or mark files as deleted. For example, one layer might add application source code, while another adds installed dependencies. The resulting image combines those changes into a single filesystem view.
Layers are generally immutable once they have been created. If a later build step changes something, Docker creates another layer rather than modifying the previous layer in place.
How Docker Image Layers Work
Docker uses a layered filesystem. Each image layer contains changes relative to the layer below it. When Docker creates a container from the image, these layers are presented as a unified filesystem.
Suppose an image contains a base Linux image, a layer with Node.js dependencies, and a layer with application files. The container does not see three separate filesystems. It sees one filesystem assembled from all of them.
The exact storage implementation depends on the container runtime and storage driver, but the important application-level concept is consistent: Docker images are composed of reusable layers.
Dockerfile Instructions and Layers
A Dockerfile describes how an image should be built. Some instructions contribute filesystem changes and therefore produce image layers, while others primarily provide metadata or affect how subsequent instructions are processed.
| Dockerfile instruction | Typical layer impact | Purpose |
|---|---|---|
| FROM | Starts from existing image layers | Defines the base image |
| RUN | Creates a filesystem layer | Executes commands during the build |
| COPY | Creates a filesystem layer | Copies files into the image |
| ADD | Creates a filesystem layer | Copies files and supports additional features |
| WORKDIR | Usually metadata | Sets the working directory |
| ENV | Usually metadata | Defines environment variables |
| ARG | Build-time configuration | Defines build arguments |
| EXPOSE | Metadata | Documents intended network ports |
| CMD | Metadata | Defines the default command |
| ENTRYPOINT | Metadata | Defines the container entrypoint |
The practical distinction is important because adding metadata does not have the same storage implications as copying files or installing packages. Docker's internal image representation can also vary between versions and builders, so it is better to focus on the filesystem changes that instructions introduce rather than memorizing a simplistic one-instruction-equals-one-layer rule.
Example: A Dockerfile With Several Layers
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["npm", "start"]The FROM instruction provides the starting image and its existing layers. The COPY of package files introduces application dependency metadata into the image, RUN npm ci adds installed dependencies, and COPY . . adds the application source. The build command then produces additional filesystem changes if the build generates files.
This structure is also useful for caching. If application source files change but package.json and package-lock.json remain unchanged, Docker can often reuse the dependency installation layer instead of running npm ci again.
Base Image Layers
When a Dockerfile begins with FROM, the new image starts from another image. That base image already contains its own layers. Your Dockerfile adds additional layers on top of them.
For example, a Node.js image may contain layers for a minimal Linux userspace, runtime libraries, and Node.js itself. Your application image then adds your dependencies and application files.
Choosing a smaller and appropriate base image can therefore have a significant effect on the final image size. However, image size should not be the only consideration. Compatibility, security updates, debugging requirements, and available system packages also matter.
What Happens When a Layer Changes?
Docker layers are immutable. If a build step produces a different result, Docker cannot simply modify the existing layer and keep its identity. Instead, the changed result is represented by a new layer or updated image configuration.
This is one reason Docker images can contain data that was later removed. Deleting a file in a later layer does not necessarily erase the bytes from the earlier layer. The final filesystem hides the deleted file, but the earlier layer can still contain its data.
Docker Image Layers and Caching
Layer caching is one of Docker's most useful build optimizations. When Docker encounters a build step whose inputs have not changed, it can reuse a previously built result instead of executing the step again.
Caching becomes especially valuable for expensive operations such as installing dependencies, compiling source code, or downloading packages.
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run buildHere, dependency files are copied before the rest of the source code. A change to a source file does not necessarily invalidate the earlier dependency installation step. If package.json or package-lock.json changes, however, npm ci needs to be executed again.
Why Dockerfile Instruction Order Matters
Docker builds proceed in order. When a build step changes, subsequent steps may also need to be rebuilt. Therefore, frequently changing files should generally be introduced later, while stable inputs and expensive operations should be placed earlier when appropriate.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run buildThis is usually more cache-friendly than copying the entire project before installing dependencies. A source-code change can invalidate COPY . ., but the dependency layer can remain reusable when the dependency manifests have not changed.
Image Layers vs Container Writable Layer
There is an important difference between image layers and the writable layer associated with a running container. Image layers come from the image and are read-only. When a container starts, Docker adds a writable layer on top of the image.
Changes made by a running container, such as creating a file inside the container filesystem, normally go into this writable layer rather than modifying the underlying image.
If the container is removed, changes stored only in its writable layer are normally removed with it. Persistent application data should therefore be stored using appropriate volumes or external storage rather than relying on the container's writable layer.
Copy-on-Write and Docker Containers
Docker's layered filesystem can use copy-on-write behavior. A file that exists in a read-only image layer does not necessarily need to be copied immediately when a container starts. If the container modifies that file, the storage system can create a writable representation for the changed data.
The exact implementation depends on the storage driver, but the general goal is the same: share unchanged image data efficiently while keeping container-specific changes separate.
Docker Layers and Image Size
Layers are reusable, but having more layers does not automatically mean a dramatically larger image. What matters most is the amount of unique data stored across the layers.
A common mistake is assuming that every RUN instruction should always be merged into one giant command solely to reduce the number of layers. Modern Docker builds have caching and other optimizations, and separate logical steps can make Dockerfiles easier to understand and maintain.
However, commands that create temporary files and then delete them in a later layer can still leave those temporary bytes in the image history. When the temporary data is not needed in the final image, creating and removing it within the same filesystem-changing step can prevent unnecessary data from becoming part of the resulting layer.
RUN apk add --no-cache curlPackage-manager-specific options such as --no-cache can also reduce unnecessary package metadata. The exact command depends on the base distribution.
Deleting Files Does Not Always Reduce Image Size
Consider a Dockerfile that first creates a large archive and then removes it:
RUN curl -O https://example.com/large-file.tar.gz
RUN rm large-file.tar.gzThe final filesystem may no longer contain the archive, but the layer that created it can still contain the original data. The later layer records the deletion. As a result, the image can remain larger than expected.
When possible, create temporary files, use them, and remove them within the same RUN instruction:
RUN curl -O https://example.com/large-file.tar.gz \
&& tar -xzf large-file.tar.gz \
&& rm large-file.tar.gzThis keeps the temporary file out of the committed result of that filesystem-changing step.
Inspecting Docker Image Layers
Docker provides commands that help inspect an image's history and understand where layers came from.
docker history my-app
docker image inspect my-appdocker history is particularly useful for seeing the commands and approximate sizes associated with image history. docker image inspect provides structured metadata about the image, including configuration and other details.
For deeper investigation, image-analysis tools can show which files occupy space and which layers contain them. These tools are useful when an image unexpectedly becomes very large.
How to Find Large Layers
When an image is larger than expected, start by examining its history. Look for steps that install large packages, copy large directories, download archives, generate build artifacts, or include development dependencies that are unnecessary at runtime.
docker history --no-trunc my-appIf a COPY instruction contributes hundreds of megabytes, inspect the build context. Files such as node_modules, .git directories, build output, logs, local caches, and environment files may have been copied accidentally.
The Role of .dockerignore
The Docker build context contains the files Docker can access during a build. A .dockerignore file prevents unnecessary files from being sent as part of that context.
node_modules
.git
.next
dist
coverage
*.log
.env
.env.localA smaller build context can make builds faster and helps prevent unwanted files from being copied into image layers. It is particularly important for projects containing dependency directories, Git metadata, generated files, or local development artifacts.
Multi-stage Builds and Layers
Multi-stage Docker builds are another important way to control the layers and contents of a production image. A build stage can contain compilers, development dependencies, source code, and other tools that are required only during compilation.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/server.js"]The final stage does not automatically inherit every filesystem layer from the builder stage. It starts from its own base image and copies only the artifacts explicitly selected with COPY --from. This makes it possible to leave build-only dependencies and tools outside the production image.
Shared Layers Between Images
One advantage of layered images is that multiple images can share identical layers. For example, several application images based on the same Node.js image can reuse the same base layers on a machine or registry.
This can reduce storage requirements and network transfer when the required layers are already available. Docker does not need to download or store another independent copy of every identical layer.
Layer sharing is one reason consistent base images can be beneficial when operating many related services.
Layers and Docker Image Distribution
When an image is pushed to or pulled from a container registry, its layers can be transferred separately. If a machine already has some required layers, it can reuse them instead of downloading identical data again.
This makes reusable layers valuable not only during local builds but also in CI/CD pipelines and deployments. Keeping commonly shared base layers stable can reduce repeated transfers.
Layer Digests and Content Addressing
Docker image data is content-addressed. Layers are identified using cryptographic digests derived from their content and related metadata. If the content changes, its digest changes as well.
Content addressing allows Docker and registries to determine whether particular image data already exists. It also helps provide integrity guarantees when images are transferred and stored.
Docker Layers and Reproducible Builds
Caching depends on Docker being able to determine whether build inputs and results are reusable. Reproducible builds make this easier to reason about because the same inputs can produce predictable outputs.
Using explicit dependency versions, lockfiles, controlled build inputs, and stable base image references can improve reproducibility. It also helps teams understand why a layer changed unexpectedly.
Common Docker Layer Mistakes
Many image-size and build-performance problems come from a few recurring Dockerfile patterns.
- Copying the entire project before installing dependencies, which can invalidate expensive cache steps after small source changes.
- Sending node_modules, .git, build output, logs, and local caches in the Docker build context.
- Adding secrets to an image and attempting to remove them in a later layer.
- Installing development dependencies in a production image when they are not required at runtime.
- Downloading large temporary files and deleting them in a later filesystem layer.
- Using a large general-purpose base image when a smaller compatible image is sufficient.
- Assuming that reducing the number of Dockerfile instructions automatically produces the smallest image.
- Failing to inspect image history when the final image unexpectedly grows.
Best Practices for Docker Image Layers
- Choose a suitable and maintained base image.
- Keep the Docker build context small with .dockerignore.
- Copy stable dependency manifests before frequently changing source files when this improves caching.
- Use lockfiles and deterministic dependency installation.
- Remove temporary data within the same filesystem-changing step when possible.
- Use multi-stage builds to keep build-only dependencies out of production images.
- Do not store secrets in Docker image layers.
- Inspect image history when diagnosing unexpected size increases.
- Avoid copying unnecessary development artifacts into the image.
- Keep Dockerfile instructions logically organized instead of optimizing only for layer count.
- Use explicit and appropriate versioning for base images and dependencies.
- Separate development and production image requirements when they differ substantially.
Layer Optimization Example
Consider a Node.js application. A less cache-friendly Dockerfile might copy the whole project before installing dependencies:
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
CMD ["npm", "start"]A more cache-friendly structure separates dependency manifests from source files:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["npm", "start"]The second version gives Docker a better opportunity to reuse the dependency installation result when only application source files change. The improvement is primarily about cache reuse rather than simply reducing the number of layers.
Docker Layers vs Docker Images
| Concept | Meaning |
|---|---|
| Layer | A filesystem change or image component that can be reused |
| Image | A complete image definition composed from a set of layers and configuration |
| Container | A running or stopped instance created from an image with its own writable layer |
| Build context | Files made available to Docker during an image build |
| Registry | A service that stores and distributes container images and their layers |
Understanding these distinctions helps explain why changing a Dockerfile can affect build time, image size, storage usage, and deployment speed in different ways.
When Should You Optimize Docker Layers?
Layer optimization is most valuable when builds are slow, images are large, CI pipelines repeatedly rebuild the same dependencies, or deployments frequently transfer large amounts of image data.
For a small personal project, a readable Dockerfile is often more important than aggressively minimizing every byte. For production applications with frequent builds and deployments, cache efficiency, image size, and predictable builds can have a much larger operational impact.
A Practical Workflow for Layer Optimization
- Build the image normally and measure its size.
- Run docker history to inspect the image history.
- Identify unusually large filesystem-changing steps.
- Check whether unnecessary files are included in the build context.
- Review the .dockerignore file.
- Move stable dependency inputs before frequently changing source files where appropriate.
- Remove unnecessary development dependencies from the runtime image.
- Use a multi-stage build when build and runtime requirements differ.
- Rebuild the image and compare the results.
- Test the final image rather than optimizing size at the expense of required runtime functionality.
Frequently Asked Questions
What is a Docker image layer?
A Docker image layer is a filesystem change that forms part of a Docker image. Layers are combined to produce the filesystem available to a container.
Does every Dockerfile instruction create a layer?
Not every instruction should be treated as an independent filesystem layer. Instructions such as RUN, COPY, and ADD commonly introduce filesystem changes, while instructions such as CMD, ENV, and EXPOSE primarily affect image configuration or metadata.
Why does Docker use layers?
Layers allow Docker to reuse unchanged image data, cache build results, share common image components, and transfer only missing layers when possible.
Does deleting a file reduce Docker image size?
Not necessarily. If the file was added in an earlier layer, deleting it in a later layer can hide it from the final filesystem while leaving its data in the earlier layer. Temporary files should be removed within the same filesystem-changing step when practical.
How can I see Docker image layers?
The docker history command is a useful starting point for inspecting image history and the approximate size associated with build steps. docker image inspect can provide additional image metadata.
Does having more Docker layers make an image larger?
Not automatically. Image size depends primarily on the unique data stored in the layers. Layer count can matter in some circumstances, but removing layers solely for the sake of reducing their number is not a complete image-optimization strategy.
How does .dockerignore help with Docker layers?
It prevents unnecessary files from entering the build context. This can reduce build overhead and helps prevent files such as node_modules, Git metadata, logs, and local build artifacts from being copied into image layers.
Helpful Docker Tools
When working with Docker images and Dockerfiles, several types of developer tools can make common tasks faster: Dockerfile generators for creating consistent starting configurations, .dockerignore generators for excluding unnecessary build-context files, Docker Compose formatters for keeping Compose files readable, .editorconfig generators for consistent editor settings, and semantic version calculators for working with dependency and image versioning.
Conclusion
Docker images are built from reusable layers that represent filesystem changes and image configuration. These layers make caching, sharing, distribution, and efficient container creation possible.
For developers, the most useful practical lessons are to understand what data each build step adds, keep the build context small, structure Dockerfiles for effective caching, avoid storing secrets in image layers, and use multi-stage builds when build-time and runtime requirements differ. Inspecting image history with Docker's built-in commands can then help identify where additional optimization is worthwhile.