Optimal Dockerfile: Faster Builds and Smaller Images

Slow builds and bloated images delay releases and complicate deployment. We rebuild images end to end: we remove unnecessary layers and dependencies, cut build time by several times, and reduce image size by half. Send a request — we'll discuss your task and come back with an estimate and work plan.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1359
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

Dockerfile optimization: why images grow and builds slow down

Image size and build time almost never come down to a single Dockerfile line — they are determined by the layer model. Each RUN, COPY, or ADD instruction creates a separate layer, and deleting a file in the next layer does not shrink it: the data remains in the previous one.

That is why apt-get update, package installation, and rm -rf /var/lib/apt/lists/* in different layers produce an image heavier than expected, even though no extra files are visible inside the container.

The second reason is the build context.

The builder receives the entire directory, including Git history, local dependencies, and artifacts from previous builds: transferring the context takes more time than the build itself, and an accidental COPY transfers junk into the final image.

Instruction order matters no less: if dependencies are installed after COPY of the whole project, any change in the source code invalidates their layer, and packages are installed again on every commit.

A heavy base adds compilers, header files, and a full set of utilities on top of that — all of it remains in the image, although only the application and its libraries are needed for operation.

Splitting build and run into several stages leaves in the result only what is required to start the process.

Dockerfile optimization is sequential work on four points: the base image, layer order, context, and stage separation. Below we break down how each of them turns into concrete changes and what that yields in build time and image size.

What a clean image gives you: build speed and predictability

Build speed is determined by how many layers are taken from cache. If you copy the entire codebase into the image before installing dependencies, the first change in the source code resets the cache and package installation starts over.

The reverse order — dependency manifest first, code second — keeps the heavy layer unchanged, and rebuilding after a source change takes seconds instead of minutes.

Image size is made up of small things: a general-purpose base image, package manager cache left inside a layer, build tools that ended up in the final image.

Multi-stage builds and cache cleanup in the same layer where packages were installed remove all of this entirely — the image shrinks by a factor, and storage consumption in the registry and on nodes stops growing with every release.

Build reproducibility is a separate effect.

Docker produces the same result only with the same inputs: the base image is pinned to an immutable identifier rather than a floating tag, dependencies are locked, and the build runs in a single environment rather than on a developer's machine.

Then the class of failures "it built in test but not in production" disappears: the artifact is built once and promoted between environments without rebuilding.

In sum, this changes not only the numbers but also the way you work. A short build lets you test changes more often, and a predictable image lets you roll back to the previous release without rebuilding.

Dockerfile optimization: comparing approaches to image builds

The choice between build approaches is a question of where we pay: build time, image size, or maintenance complexity. A single-stage build is set up in one pass but leaves compilers and auxiliary packages in the final layer. A multi-stage build yields a smaller image but requires discipline in caching and artifact copying.

What we compare

Option When it fits Limitations
Single-stage build Simple service, scripts, static assets; the dependency set is the same for build and runtime Build tools remain in the image: it grows, and with it deployment time and the number of vulnerable packages
Multi-stage build Code is compiled or assembled into a single artifact, and only the result is needed at runtime Harder to read and maintain; if copying is careless, extra files end up in the final layer
Thin final layer A minimal base image and predictable size are needed Every library is added manually; a missed dependency only shows up at startup
Build from a ready distribution There is a verified artifact from a vendor or a separate build stage The image is not reproducible from source: the build step lives separately and requires its own maintenance

We choose based on the contents of the runtime environment: if the application needs the same packages as the builder, stage separation provides no gain.

If the output is a single binary or a set of static assets, a multi-stage scheme and a thin final layer reduce the image and speed up deployment.

Rebuild stages: from layer measurement to handover of the rules

We work on the image not "by eye" but from measurements: first we record how long the build takes with an empty and warm layer cache and what makes up the image size, then we change it step by step, checking the result after each one.

Each stage ends with a measurable outcome — build time, image size, the share of steps that run without manual intervention.

  1. Build audit. We examine the current build file and log: which layers rebuild when one line of code changes, where cache is lost, how long a full and repeated build takes, and how much the image weighs in the registry.
  2. Layer scheme. We arrange the image by frequency of change: system packages, project dependencies, source code, configuration. Rare changes go lower, changeable ones go higher, so working layers are reused rather than rebuilt.
  3. Instruction order. We rearrange the steps according to this scheme, copying only the necessary files rather than the whole directory. Build tools and temporary dependencies remain in a separate stage and do not end up in the final image.
  4. Build configuration. Rules for excluding unnecessary items, a minimal base image, pinned dependencies, a single point for installing them — so the image does not grow from build to build.
  5. Image verification. We check the size, number of layers, absence of debugging utilities and service files, and build time with a clean cache and a repeated build. Additionally, we run a scan for vulnerable packages and start the service in an isolated environment.
  6. Repeatability check. Two builds in a row on different machines produce the same result, without manual steps or local dependencies — otherwise the gain will not stick.
  7. Handover of the rules. We hand over the build file, exclusion rules, layer description and acceptance criteria, and a short guide "how to change and what to check" — so the team can maintain the result itself.

What you receive: configurations, schemes, and rules

The work ends not with the advice "combine commands into one line" but with a set of files and rules according to which the build and image continue to live without us. Below is what exactly we hand over and why each artifact is needed in the team's daily work.

  • Build configuration — a working file with ordered layers and dependency caching: a code change no longer triggers package reinstallation, and rebuilding takes a few minutes.
  • Layer description — a map of where each layer sits and what resets the cache above it; an engineer immediately sees why changing one file rebuilds half the image.
  • Rules for working with base images — which image we consider the reference, where we get updates, how we verify it before changing the tag, and what we pin strictly versus update in a controlled way.
  • Rules for working with dependencies and artifacts — the order of copying files and the separation of build and run stages so that compilers and temporary files do not end up in the final image.
  • Build documentation — local run commands, environment variables, entry points, and common errors; a new engineer builds the image independently, without verbal hints.
  • Update rules — the frequency of rebuilding base layers, the vulnerability check procedure, and the rules for rolling back to the previous image.
  • Pipeline check scheme — which steps block an image from entering the registry and which merely warn.

All of this remains in your repository and reads like an instruction, not like a chat conversation.

Case study: builds sped up several times over, and the image became half the size

The project is an internal service with native extensions and a noticeable dependency tree. The build on every commit took 18 minutes, and the finished image was 2.5 times larger than the code itself: compilers, package manager cache, and test data ended up in it.

Every tenth run failed due to a mismatch in system library versions between the build and runtime environments, and investigating the cause took up to half an hour.

What we changed

The Docker build was split into stages: dependencies are installed first, then the code is copied, so editing one file no longer resets the layer cache.

The final image is built from a minimal base — only artifacts are copied into it, while compilers, test data, and package manager cache remain in the previous stage. System package versions are pinned and checked during the build, so the library sets in build and runtime match.

What happened as a result

A build after a code change takes 4 minutes instead of 18; when only configuration changes, it takes less than a minute. The image became 2.6 times smaller, and the number of layers dropped from 30 to 11, so pushing to nodes and container startup sped up by the same factor.

The share of startup failures decreased from 12% to 1%: the "library mismatch" failure class disappeared entirely, leaving only configuration errors of the service itself.

How does a multi-stage build differ from a single-stage build in practice?

In a single-stage build, the compiler, sources, tools, and dependency cache remain in the same image as the running application. The final image contains everything that was needed during the build, including utilities that are never invoked in operation.

The final image size grows by a factor, and with it the attack surface and the time to push the image to nodes.

A multi-stage build separates these roles. The build stage installs tools and compiles, while the final stage starts from a minimal base and receives the result by transferring artifacts between stages — by copying from the previous stage.

Only the compiled binaries and the runtime end up in the final image, so the size is reduced to the size of the code, and unnecessary utilities do not get into the container.

We pay for this with maintenance complexity. There become two environments — build and runtime — and the versions in them must be kept consistent: a mismatch only manifests at runtime, not during the build.

Stage caching works differently, and intermediate layers are not visible in the final image, so the usual debugging approach of "go into the container and look" stops working — a separate stage is added for it.

The transition is not always justified. If the application is not compiled, the artifact comes ready-made from outside, or the image lives only in an internal environment where size is not critical, a single-stage build gives a more predictable and simpler result.

The decision is made based on measurements: how many layers and how much volume tool installation adds, and how build time changes after stage separation.

Questions asked before rebuilding images

Before rebuilding images, the same doubts come up: whether to rewrite the build entirely, how to make sure the image has not lost functionality, and what to do with the old configuration.

Below are short answers for each point, so the decision is made before the changes, not after an unsuccessful release.

Do I need to rewrite the build entirely?

No, and it is usually an unnecessary risk. We start by analyzing the layers: what changes with every release and what has been stable for months.

Dependencies that change rarely are placed higher in the file, application code lower, so the layer cache is reused and rebuilding happens only for the changed part. Then come targeted changes, and each is confirmed by measuring build time and image size.

How do I check that the rebuild did not break anything?

We compare the image before and after by formal attributes: the set of installed packages, entry points, open ports, environment variables, and the user's permissions inside the container.

Additionally, we run the change check in the pipeline — the same tests as on the release branch. A discrepancy in any of the attributes is investigated before deployment, not after.

What should be done with the old configuration?

Configuration migration happens in stages: the old and new variants coexist for a while, with switching done one parameter at a time. The previous image remains in the registry with a live tag, so rollback takes minutes.

Separately, we record which settings moved into the image and which remained in the external environment — otherwise maintaining the build turns into guesswork.

Who is responsible for the build after handover?

The build rules are in the repository next to the code, not in a chat. Each non-obvious layer has a short comment: why it exists and what will happen if it is removed. Your team edits the file independently; our engineers get involved for changes affecting the base image.

Let's discuss the task: what to send for an estimate and work plan

The accuracy of the work estimate depends on the input data: the fuller the picture of the current pipeline, the fewer assumptions in the calculation. Below is the minimum set on which we return with a work plan, stages, and order of changes. Some items may be missing — just say so; for the first iteration, some data is not critical.

  • The current Dockerfile in full — base image, build stages, instruction order.
  • Build log — duration by step and image size before changes.
  • Repository structure — where dependencies, manifests, and version pinning files are located.
  • How the pipeline is set up — what runs the build and whether the layer cache is preserved between runs.
  • Environment requirements — which services are needed during operation and what is written to disk.
  • Constraints — private registry, closed networks, security rules, artifact size limits.
  • Definition of done — the target build time and image size at which the task is considered complete.

If some data is closed, the project can start with what is available: we will analyze the current build and come back with an estimate and plan.

A build consultation is part of that analysis: we show which layers and step order give the main gain in time and size, what can be moved into the base image, and what can be removed entirely. Describe the task — we will come back with an estimate and plan.