| name | image-optimization |
|---|---|
| description | Reduce Docker image size with base image selection, layer consolidation, multi-stage patterns, and distroless builds. |
| standards-version | 1.6.3 |
Use this skill when:
- A Docker image is too large for fast pulls or deployments
- CI/CD pipelines are slow due to image size
- Comparing base image options (Alpine vs slim vs distroless vs scratch)
- Analyzing image layers to find bloat
- Applying multi-stage build patterns to strip build-time dependencies
- Using BuildKit features for more efficient builds
- Current image name/tag or Dockerfile to optimize
- Application language/runtime
- Whether the image needs a shell or debugging tools in production
- Target environment (Kubernetes, ECS, local dev)
- Measure the current image - use
docker_listImagesto get the current size, thendocker_inspectImagefor layer details. - Analyze layers - use
dive(if installed) ordocker historyto find which layers contribute the most size. - Choose the right base image - evaluate Alpine, slim, distroless, and scratch against your requirements.
- Apply multi-stage builds - separate build and runtime stages so compilers, dev headers, and source code stay out of the final image.
- Consolidate RUN layers - chain commands with
&&and clean up caches in the same layer. - Create a .dockerignore - prevent build context bloat by excluding unnecessary files.
- Use BuildKit cache mounts - mount package manager caches to avoid re-downloading on every build.
- Rebuild and compare - build the optimized image and compare sizes using
docker_listImages.
- Multi-stage builds
- BuildKit documentation
- Distroless images
- dive - image layer explorer
- Docker image best practices
User: My Node.js image is 1.2GB. Help me shrink it.
Agent: That's likely using node:20 (Debian full). Let me walk through the optimization.
Base image comparison for Node.js:
| Base Image | Approximate Size |
|---|---|
node:20 |
~1.1GB |
node:20-slim |
~200MB |
node:20-alpine |
~130MB |
| Distroless (gcr.io/distroless/nodejs20-debian12) | ~120MB |
Optimized multi-stage Dockerfile:
# syntax=docker/dockerfile:1
# Stage 1: Install production dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
# Stage 2: Build the application
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
# Stage 3: Production runtime
FROM node:20-alpine AS runtime
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=deps --chown=app:app /app/node_modules ./node_modules
COPY --from=build --chown=app:app /app/dist ./dist
COPY --chown=app:app package.json ./
USER app
EXPOSE 3000
CMD ["node", "dist/index.js"]User: Can I go even smaller with distroless?
Agent: Yes, distroless drops the shell and package manager entirely:
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
EXPOSE 3000
CMD ["dist/index.js"]Trade-off: no shell means you cannot docker exec into the container for debugging. Use a debug variant (gcr.io/distroless/nodejs20-debian12:debug) in staging if needed.
User: How do I analyze which layers are biggest?
Agent:
# Quick layer breakdown
docker history my-app:latest --human --no-trunc
# Interactive analysis with dive
dive my-app:latestOr use MCP tools:
Call docker_inspectImage with imageName "my-app:latest" to see
layer digests and total size. Then compare against the optimized
build.
Go application (scratch base):
FROM golang:1.22-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]This produces a final image of just the static binary plus CA certs, typically under 20MB.
Python application (slim base):
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=build /install /usr/local
COPY . .
RUN useradd -r app
USER app
CMD ["python", "main.py"]| Tool | Purpose |
|---|---|
docker_listImages |
Compare image sizes before and after optimization |
docker_inspectImage |
Examine layer count, labels, and detailed size breakdown |
docker_searchHub |
Find alternative base images (Alpine, slim, distroless variants) |
docker_diskUsage |
Check total disk used by images and build cache |
docker_systemInfo |
Verify BuildKit is enabled for cache mount support |
Before/after comparison workflow:
1. Call docker_listImages - note the size of the current image.
2. Build the optimized image with a new tag.
3. Call docker_listImages again - compare the two sizes.
4. Call docker_inspectImage on both to compare layer counts.
Finding slim base images:
Call docker_searchHub with query "python" to find official images,
then check available tags for slim and alpine variants.
- Alpine + glibc apps - Alpine uses musl libc. Python packages with C extensions, Java apps, or anything linked against glibc may fail or perform poorly. Test thoroughly or use slim instead.
- Distroless with no escape hatch - distroless images have no shell. If you need to debug in production, keep a debug-tagged variant available or use ephemeral debug containers.
- Cleaning caches in a separate layer -
RUN npm installfollowed byRUN rm -rf /root/.npmdoes not save space. The cache is already baked into the first layer. Clean up in the same RUN instruction. - Not using .dockerignore - without it, your entire source tree (including .git, node_modules, test fixtures) is sent as build context, slowing builds even if not copied.
- Forgetting CA certificates in scratch - scratch images have nothing. If your app makes HTTPS calls, copy CA certs from the build stage.
- Using
COPY . .in the final stage - this copies source code, tests, and configs you don't need at runtime. Be explicit about what goes into the final image. - BuildKit cache mounts not enabled -
--mount=type=cacherequires BuildKit. SetDOCKER_BUILDKIT=1or use the# syntax=docker/dockerfile:1parser directive. - Layer squashing as a silver bullet -
--squashmerges layers but removes cache reuse for intermediate layers. It's rarely worth the trade-off. Multi-stage builds are better. - Ignoring multi-platform builds - if you target both amd64 and arm64, test your optimized image on both architectures. Alpine and distroless support multi-arch, but some base images don't.
- Stripping too aggressively -
go build -ldflags="-s -w"strips debug symbols, which is fine for production but makes crash analysis harder. Keep unstripped builds for staging.