← Back to list

9 Docker Image Optimization Tips That Actually Cut Build Times (2026)

At some point on almost every project I’ve been around, someone does a docker images and goes quiet for a second. Then they say something…

Himanshu Pant · 2026-07-03 06:16 · 0 claps · 6.7 min read
#docker-optimization #build-performance #docker #multi-stage-builds #innostax
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

9 Docker Image Optimization Tips That Actually Cut Build Times (2026)

At some point on almost every project I’ve been around, someone does a docker images and goes quiet for a second. Then they say something like "why is this 2.3 GB." And nobody has a clean answer, because it didn't happen all at once — it accumulated. A base image nobody questioned. A COPY instruction that grabbed the whole repo. Build tools that got installed and never cleaned up. Six months of small decisions that felt fine individually.

Docker itself is fine. It’s genuinely useful and the containerization model holds up. The problem is that nothing in the default workflow stops you from building something enormous. You have to opt into lean.

What follows are nine things worth fixing. They’re ordered roughly by impact — the first three matter most, the rest are table stakes once those are done.

Before You Touch Anything: Understand What You’re Actually Dealing With

A Docker image is layers. Every RUN, COPY, and ADD in your Dockerfile writes a new one. Those layers cache — which is the thing that makes incremental builds tolerable — but the cache doesn't help when your base image is already 400 MB, or when you've stacked 30 layers doing things that could've been one.

The mental model that helps most: think of each layer as a snapshot you’re committing and shipping forever. Files added in layer 6 don’t disappear if you delete them in layer 9. They’re still in layer 6. The image carries all of it.

That’s why cleanup-after-the-fact doesn’t work the way people expect, and why base image choice hits you early and everywhere.

1. The Base Image You Picked Is Probably Doing More Damage Than Everything Else Combined

Nobody audits the base image. It’s just there, it works, moving on.

ubuntu:latest is somewhere around 70–80 MB before a single dependency goes in. That doesn't sound catastrophic until you factor in all the layers on top of it — and the fact that you're pulling and pushing that weight on every build, every deploy, every environment. alpine:latest is 5 MB. Not 50. Five.

# You've opted into an entire OS you probably don't need
FROM ubuntu:latest
# This is almost always enough
FROM alpine:latest

Alpine isn’t right for everything — some compiled dependencies are annoying to build on musl libc, and you’ll occasionally hit a missing library that costs you an hour. For those cases, use a slim variant instead: node:18-slim, python:3.11-slim. Still dramatically smaller than the defaults. Worth the five minutes it takes to check.

Pick the smallest image that actually runs your app. Not the one that seemed like a safe default.

2. Every Separate RUN Line Is a Layer You’re Paying For

This one looks small on paper and adds up faster than people expect.

# Three instructions. Three layers. No reason.
FROM alpine:latest
RUN apk update
RUN apk add --no-cache curl

Versus:

FROM alpine:latest
RUN apk update && apk add --no-cache curl

Same result, one layer. But there’s a second problem with the split version that’s less obvious: apk update in layer N gets cached. If you run the build again later, Docker might serve the cached package index from that layer — which is now stale — while pulling new packages in layer N+1. You end up with a partially inconsistent install and a confusing failure that's annoying to debug. Keeping them in one instruction sidesteps this entirely.

3. If You’re Not Using Multi-Stage Builds, Stop What You’re Doing and Fix That First

This is the one. If I had to pick a single change that makes the biggest difference on the most projects, it’s this.

The idea is that you don’t have to ship everything needed to build your app in the image that runs your app. Use one FROM to compile, bundle, whatever — then use a second FROM to create the actual runtime image and copy only the output across. Everything else stays behind.

FROM node:14 AS builder
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

What goes out the door: dist files, inside nginx:alpine. That’s it.

What gets left behind: Node.js, node_modules, your entire source tree, every dev dependency, the npm cache. For a mid-sized React app, the before/after is usually something like 900 MB → 25 MB. That’s not a marginal improvement. That’s a different category of image.

(And yes — if you’re already doing this and your images are still large, go back to tip 1.)

4. COPY . Is Lazy and You Know It

Look, COPY . /app is fast to type. But it copies whatever lives in that directory at build time — and depending on the project, that's your .env.local with secrets in it, your test data, your node_modules if they happened to be there, the Figma exports someone saved to the repo, whatever.

# This
COPY . /app
# vs this
COPY src/ /app/src
COPY public/ /app/public
COPY package.json /app/

Be specific about what the running app actually needs. If you’re not sure, check — don’t guess. Also get a .dockerignore in place. Works the same as .gitignore, stops unwanted files from entering the build context in the first place, and cuts the time Docker spends just reading the context before a build even starts.

5. Fragmented Installs Are a Quiet Cache Killer

RUN npm install
RUN npm install --global some-package

Two layers, two cache entries, two chances for something to go stale or behave differently across machines. Just chain them.

RUN npm install \
    && npm install --global some-package

While you’re at it — if your package manager leaves behind a cache directory (npm, pip, apt all do), wipe it in the same instruction. Cleaning it up in a later RUN does nothing; the original layer already committed those files. You'd just be adding a layer that pretends to remove weight it can't actually touch.

6. latest Will Eventually Betray You

The tag is a lie. Or not a lie exactly — but it’s a moving target that has no obligation to stay compatible with anything.

FROM node:latest   # today this is Node 22; next month who knows
FROM node:14       # this is Node 14, now and at every future build

I’ve seen this break CI on a Monday morning because a base image updated over the weekend and introduced a breaking change nobody expected. Debugging it took most of the day because nobody initially thought to check whether the base image had changed — why would you, nobody touched the Dockerfile. Pin your versions. Two seconds now, potentially hours saved later.

7. BuildKit — Just Turn It On

export DOCKER_BUILDKIT=1

BuildKit is Docker’s newer build backend. It runs independent stages in parallel, handles caching more intelligently, and generally produces faster builds. It’s not new at this point — it’s been available for years — but it still isn’t the default in all environments, particularly older CI setups.

For a single-stage Dockerfile with three instructions, you won’t notice. For a multi-stage build with five or six stages, some of which don’t depend on each other, the parallel execution saves meaningful time. In CI where you’re paying per minute, that compounds fast.

8. The Cleanup Trick That Doesn’t Work (And the One That Does)

A surprisingly common pattern:

RUN apk add --no-cache build-base \
    && make \
    && apk del build-base

Looks reasonable. The problem: Docker committed that first layer when build-base was installed. apk del runs in a new layer that records a deletion, but the original layer still exists in the image history. The tools are "gone" from the container's perspective but the image size didn't shrink.

RUN apk add --no-cache build-base \
    && make \
    && apk del build-base \
    && rm -rf /var/cache/apk/*

Everything in one RUN. Install, use, remove, wipe the cache — all one layer, all one commit. Now the layer only contains the output of make, not the gigabytes of build tooling that produced it.

9. Layer Order Is Free Performance

Docker invalidates cache from the first changed layer onward. If you copy your entire source tree before installing dependencies, you’re reinstalling every dependency on every single build — because something in that source tree almost certainly changed.

# Wrong order — any file change triggers full reinstall
COPY . /app
RUN npm install
# Right order — reinstall only when package.json changes
COPY package.json /app/
RUN npm install
COPY . /app/

The source tree changes constantly. package.json doesn't. Copy the thing that changes least, run the expensive step, then copy the thing that changes often. You get cache hits on npm install across most builds, and the full reinstall only triggers when you actually add or update a dependency.

Where To Start If This All Feels Like a Lot

It isn’t, really. Two things: base image and multi-stage builds. Do those, measure your image size, and you’ll likely be satisfied.

Everything else here is worth picking up, but in order of effort-to-impact, the first three tips are a different tier from the rest. Layer ordering and version pinning are maintenance hygiene — good habits, low drama. Cleanup-in-one-layer is something you do once per Dockerfile and forget about.

The main thing is to actually look at your image sizes periodically. Most teams don’t, and the numbers drift in one direction.

Read more — Docker Image Optimization Tips

Himanshu Pant Chief Operating Officer at Innostax

About Innostax

Innostax is a global software consulting and custom software development company helping growth-stage startups, scaleups, and enterprises build reliable, scalable digital products. Founded in 2014 and headquartered in Framingham, Massachusetts, Innostax specializes in custom software development, web and mobile app development, IT staff augmentation, offshore software development, and digital transformation services — across industries including healthcare, retail, education, travel, and fintech. With a dedicated development team model, a 2-week risk-free trial, and deep expertise in technologies like React.js, Node.js, Python, .NET, and React Native, Innostax co-creates breakthrough solutions that help founders, CTOs, and product leaders ship better software, faster. Learn more at innostax.com.


메타데이터
post_id
b4f0b8a87bed
slug
9-docker-image-optimization-tips-that-actually-cut-build-times-2026-b4f0b8a87bed
url
https://medium.com/@himanshu.pant415/9-docker-image-optimization-tips-that-actually-cut-build-times-2026-b4f0b8a87bed
canonical_url
https://medium.com/@himanshu.pant415/9-docker-image-optimization-tips-that-actually-cut-build-times-2026-b4f0b8a87bed
author_url
https://medium.com/@himanshu.pant415
status
ok
fetched_at
2026-09-04 11:21:07