The Tools: docker image history + dive
Starting With a Fat Image
Step 1: docker image history
Step 2: dive to Find the Waste
Fix What's Actually Broken
Can You Go Further?
Go: A Different Story
Summary
References Your Docker image is large and you don't know why.
Guessing and applying a checklist of tips might not cut much.Find out what's actually causing the size, then fix that specifically. Two tools, different purposes. docker image history is built into Docker. It shows how much space each layer takes: dive is a third-party tool. It lets you browse each layer's contents interactively and reports how much space is being wasted: A typical Node.js Dockerfile with no optimizations: package.json has a few devDependencies: jest, typescript, @types/express. After docker build, docker images shows: 1.25GB. Don't touch the Dockerfile yet — find out where the weight is. Output (relevant lines): Three things stand out immediately: docker image history shows layer sizes. dive shows what's actually inside each layer: 107MB wasted. dive names the culprits directly: typescript, @babel/parser — all devDependencies that serve no purpose in a production image. "Count: 2" means the same file appears in two layers — once from npm install, once from COPY . .. That's what happens without a .dockerignore: node_modules gets installed, then copied in again on top. Problems identified. Fix each one: Problem 1: node_modules copied twice→ Add .dockerignore Problem 2: devDependencies in production image→ Multi-stage build, production stage uses --omit=dev Problem 3: Base image is too heavy (Debian + build tools)
→ Switch to node:20-alpine The fixed Dockerfile: Run dive again to confirm: The 139MB floor is mostly the Node.js runtime inside node:20-alpine. To go lower, switch to distroless: That gets you to around 100MB. Beyond that, the gains are small — unless you switch to Go. Go compiles to a static binary. Use scratch — a completely empty base image: Final image is just the binary. A few MB to a few dozen MB. At this point dive has little to tell you — there's almost nothing to optimize. Workflow: docker image history to find the heavy layers → dive to see what's inside them → fix the actual problem. Use tools to diagnose, make targeted fixes, then verify with tools again. More effective than guessing. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
$ -weight: 500;">docker image history <image-name>
-weight: 500;">docker image history <image-name>
-weight: 500;">docker image history <image-name>
# Install
-weight: 500;">brew -weight: 500;">install dive # macOS
-weight: 500;">apt -weight: 500;">install dive # Ubuntu (requires adding repo first) # Analyze interactively
dive <image-name> # CI mode (report only, no interactive UI)
CI=true dive <image-name>
# Install
-weight: 500;">brew -weight: 500;">install dive # macOS
-weight: 500;">apt -weight: 500;">install dive # Ubuntu (requires adding repo first) # Analyze interactively
dive <image-name> # CI mode (report only, no interactive UI)
CI=true dive <image-name>
# Install
-weight: 500;">brew -weight: 500;">install dive # macOS
-weight: 500;">apt -weight: 500;">install dive # Ubuntu (requires adding repo first) # Analyze interactively
dive <image-name> # CI mode (report only, no interactive UI)
CI=true dive <image-name>
FROM node:latest WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm -weight: 500;">install
COPY . .
CMD ["node", "index.js"]
FROM node:latest WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm -weight: 500;">install
COPY . .
CMD ["node", "index.js"]
FROM node:latest WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm -weight: 500;">install
COPY . .
CMD ["node", "index.js"]
REPOSITORY TAG IMAGE ID SIZE
demo-app latest ddb21d14ccef 1.25GB
REPOSITORY TAG IMAGE ID SIZE
demo-app latest ddb21d14ccef 1.25GB
REPOSITORY TAG IMAGE ID SIZE
demo-app latest ddb21d14ccef 1.25GB
-weight: 500;">docker image history demo-app
-weight: 500;">docker image history demo-app
-weight: 500;">docker image history demo-app
CREATED BY SIZE
CMD ["node" "index.js"] 0B
COPY . . 49.3MB
RUN -weight: 500;">npm -weight: 500;">install 61MB
COPY package*.json ./ 166kB
WORKDIR /app 0B
RUN ... (node binary -weight: 500;">install) 199MB
RUN ... (-weight: 500;">apt-get build-essential etc.) 561MB
RUN ... (-weight: 500;">apt-get base packages) 184MB
# debian bookworm base 139MB
CREATED BY SIZE
CMD ["node" "index.js"] 0B
COPY . . 49.3MB
RUN -weight: 500;">npm -weight: 500;">install 61MB
COPY package*.json ./ 166kB
WORKDIR /app 0B
RUN ... (node binary -weight: 500;">install) 199MB
RUN ... (-weight: 500;">apt-get build-essential etc.) 561MB
RUN ... (-weight: 500;">apt-get base packages) 184MB
# debian bookworm base 139MB
CREATED BY SIZE
CMD ["node" "index.js"] 0B
COPY . . 49.3MB
RUN -weight: 500;">npm -weight: 500;">install 61MB
COPY package*.json ./ 166kB
WORKDIR /app 0B
RUN ... (node binary -weight: 500;">install) 199MB
RUN ... (-weight: 500;">apt-get build-essential etc.) 561MB
RUN ... (-weight: 500;">apt-get base packages) 184MB
# debian bookworm base 139MB
CI=true dive demo-app
CI=true dive demo-app
CI=true dive demo-app
efficiency: 95.49 %
wastedBytes: 107 MB
userWastedPercent: 9.68 % Inefficient Files:
Count Wasted Space File Path 2 18 MB /app/node_modules/typescript/lib/typescript.js 2 12 MB /app/node_modules/typescript/lib/_tsc.js 2 3.7 MB /app/node_modules/typescript/lib/lib.dom.d.ts 2 2.9 MB /app/node_modules/@babel/parser/lib/index.js.map ...
efficiency: 95.49 %
wastedBytes: 107 MB
userWastedPercent: 9.68 % Inefficient Files:
Count Wasted Space File Path 2 18 MB /app/node_modules/typescript/lib/typescript.js 2 12 MB /app/node_modules/typescript/lib/_tsc.js 2 3.7 MB /app/node_modules/typescript/lib/lib.dom.d.ts 2 2.9 MB /app/node_modules/@babel/parser/lib/index.js.map ...
efficiency: 95.49 %
wastedBytes: 107 MB
userWastedPercent: 9.68 % Inefficient Files:
Count Wasted Space File Path 2 18 MB /app/node_modules/typescript/lib/typescript.js 2 12 MB /app/node_modules/typescript/lib/_tsc.js 2 3.7 MB /app/node_modules/typescript/lib/lib.dom.d.ts 2 2.9 MB /app/node_modules/@babel/parser/lib/index.js.map ...
node_modules
.-weight: 500;">git
.env
*.log
node_modules
.-weight: 500;">git
.env
*.log
node_modules
.-weight: 500;">git
.env
*.log
# Stage 1: -weight: 500;">install everything (including devDeps for build)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci
COPY . .
# If you have TypeScript: RUN -weight: 500;">npm run build # Stage 2: production dependencies only
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci --omit=dev # no jest, no typescript
COPY --from=builder /app/index.js ./
CMD ["node", "index.js"]
# Stage 1: -weight: 500;">install everything (including devDeps for build)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci
COPY . .
# If you have TypeScript: RUN -weight: 500;">npm run build # Stage 2: production dependencies only
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci --omit=dev # no jest, no typescript
COPY --from=builder /app/index.js ./
CMD ["node", "index.js"]
# Stage 1: -weight: 500;">install everything (including devDeps for build)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci
COPY . .
# If you have TypeScript: RUN -weight: 500;">npm run build # Stage 2: production dependencies only
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci --omit=dev # no jest, no typescript
COPY --from=builder /app/index.js ./
CMD ["node", "index.js"]
-weight: 500;">docker build -t demo-app-fixed .
-weight: 500;">docker images | grep demo
-weight: 500;">docker build -t demo-app-fixed .
-weight: 500;">docker images | grep demo
-weight: 500;">docker build -t demo-app-fixed .
-weight: 500;">docker images | grep demo
REPOSITORY SIZE
demo-app-fixed 139MB ← down from 1.25GB
demo-app 1.25GB
REPOSITORY SIZE
demo-app-fixed 139MB ← down from 1.25GB
demo-app 1.25GB
REPOSITORY SIZE
demo-app-fixed 139MB ← down from 1.25GB
demo-app 1.25GB
efficiency: 99.96 %
wastedBytes: 75 kB ← down from 107MB
efficiency: 99.96 %
wastedBytes: 75 kB ← down from 107MB
efficiency: 99.96 %
wastedBytes: 75 kB ← down from 107MB
FROM gcr.io/distroless/nodejs20-debian12 AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/index.js ./
CMD ["index.js"]
FROM gcr.io/distroless/nodejs20-debian12 AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/index.js ./
CMD ["index.js"]
FROM gcr.io/distroless/nodejs20-debian12 AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/index.js ./
CMD ["index.js"]
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server . FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server . FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server . FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"] - 561MB -weight: 500;">apt-get layer: build-essential, python3, gcc — build tools that aren't needed at runtime, but they're stuck in the image
- 61MB -weight: 500;">npm -weight: 500;">install: includes devDependencies (jest, typescript) that production doesn't use
- 49.3MB COPY . .: node_modules got copied in (no .dockerignore) - Bloated base image: node:latest (Debian) → node:20-alpine
- devDependencies in production: multi-stage build + --omit=dev
- Duplicate node_modules: add .dockerignore - dive on GitHub — Docker image layer explorer
- Docker Docs — Multi-stage builds
- Docker Docs — .dockerignore file
- GoogleContainerTools Distroless images
- Docker Docs — -weight: 500;">docker image history