Skip to main content

Як зменшити розмір Docker image?

Зменшення розміру Docker image це частково гігієна, частково архітектура. Правильні техніки на правильну проблему можуть зменшити image з 1 GB до 30 MB без втрати функціоналу. Менші image це швидші pull, швидші деплої, менша поверхня атаки.

Теорія

TL;DR

П'ять технік, у приблизному порядку impact:

  1. Multi-stage build з тонким фінальним base (alpine, distroless, scratch). Найбільший single-win.
  2. Менший base image: Debian slim → Alpine → distroless → scratch. Кожен крок ~50-100 MB менше.
  3. Single RUN для install + cleanup, щоб cache-файли не запекалися у шар.
  4. .dockerignore для тримання build context малим (без node_modules, .git тощо).
  5. Скинь dev-deps, recommended-пакети, невикористані локалі у runtime-стейджі.

Міряй через docker images і docker history. Для глибокого аналізу dive.

Швидкий приклад: до і після

До (single stage, наївно):

dockerfile
FROM node:22 WORKDIR /app COPY . . RUN npm install RUN npm run build CMD ["npm", "start"]

Фінал: ~1.2 GB.

Після (multi-stage, alpine, prune):

dockerfile
FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build RUN npm prune --omit=dev FROM node:22-alpine WORKDIR /app COPY --from=build /app/dist /app/dist COPY --from=build /app/node_modules /app/node_modules USER node CMD ["node", "dist/server.js"]

Фінал: ~180 MB. Той самий функціонал.

Для статичних сайтів (Node-runtime не треба):

dockerfile
# Стейдж 2: FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html

Фінал: ~30 MB.

Техніка 1: multi-stage з тонким фінальним base

Див. окрему статтю про multi-stage. Підсумок: toolchain це найважче у твоєму image; multi-stage це як його лишити позаду.

Варіанти base-image для фінального стейджу, у порядку розміру:

BaseПрибл. розмірМає shell?Має package manager?
debian:bookworm120 MBТак (bash)apt
debian:bookworm-slim75 MBТак (bash)apt
ubuntu:24.0480 MBТак (bash)apt
alpine:3.217-8 MBТак (sh, busybox)apk
gcr.io/distroless/base20 MBНіНі
gcr.io/distroless/static2 MBНіНі
scratch0НіНі

Обирай найменше, що має те, що твоєму бінару реально треба.

Техніка 2: об'єднуй RUN-команди і чисти cache

dockerfile
# НЕПРАВИЛЬНО: кожен RUN це шар; apt-cache виживає у шарі 2 RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # ПРАВИЛЬНО: один шар, cache видалено у тому ж кроці RUN apt-get update && \ apt-get install -y --no-install-recommends curl && \ rm -rf /var/lib/apt/lists/*

Неправильна версія не економить місце, шар 2 тримає apt-cache, шар 3 лише додає whiteout-маркери (cache-файли все ще на диску).

Застосовуй той самий патерн до:

  • apk (Alpine): apk add --no-cache <pkg> (авто-чистить)
  • pip: pip install --no-cache-dir <pkg>
  • npm: npm ci --only=production && npm cache clean --force

Техніка 3: .dockerignore

Усе у твоєму build context шиппиться на daemon, сповільнюючи білди і роздуваючи шари. Типовий .dockerignore:

.git node_modules dist *.log .env* Dockerfile* README.md coverage .vscode .idea

Без цього COPY . . шле гігабайти, що не треба.

dockerfile
# Node RUN npm ci --omit=dev # Python RUN pip install --no-cache-dir --prefix=/install <pkgs> # Тоді у фінальному стейджі COPY лише /install # Go: нічого робити (бінар self-contained) # apt з --no-install-recommends RUN apt-get install --no-install-recommends -y curl

Dev-deps (TypeScript-компілятор, jest, eslint) часто подвоюють node_modules. --no-install-recommends ріже опційні пакети apt.

Техніка 5: мінімізуй що COPY'ється

dockerfile
# Granular копіювання менше І краще для cache COPY package*.json ./ # тільки lock-файли → install RUN npm ci COPY src/ ./src/ # тільки те, що треба runtime COPY public/ ./public/

Vs. COPY . ., що копіює тести, docs, IDE-config, build-output.

Inspecting і пошук bloat

bash
# Per-layer розміри $ docker history --no-trunc myimage IMAGE CREATED CREATED BY SIZE 4f06b3e2c0c1 2 minutes ago /bin/sh -c #(nop) CMD ["node" "server.js"] 0B <missing> 2 minutes ago /bin/sh -c npm prune --omit=dev 156MB ← атакуй це <missing> 3 minutes ago /bin/sh -c npm run build 34MB <missing> 4 minutes ago /bin/sh -c npm ci 312MB ← найбільший винуватець ... # Інтерактивний layer-by-layer погляд $ dive myimage # Показує added/removed/total байти кожного шару, file-tree на шар.

dive золотий стандарт для розуміння, чому image такий, який є.

Типові помилки

Додавати файли у одному шарі, видаляти у іншому

dockerfile
# НЕПРАВИЛЬНО: 200 MB ще у шарі N, шар N+1 лише його ховає ADD bigfile.tar.gz /tmp/ RUN unpack-and-process /tmp/bigfile.tar.gz RUN rm -rf /tmp/* # whiteout, але дані у шарі N назавжди # ПРАВИЛЬНО: усе в одному шарі RUN mkdir -p /tmp/x && \ curl -L https://... | tar xz -C /tmp/x && \ process /tmp/x && \ rm -rf /tmp/x

Шари незмінні. Як файл попав у шар, жоден пізніший шар не зменшить image, лише оригінальний шар може уникнути файлу.

Використання apt без --no-install-recommends

Apt Debian встановлює «recommended»-пакети за замовчуванням. Для server-image майже жоден не потрібен. Завжди:

dockerfile
RUN apt-get update && \ apt-get install --no-install-recommends -y curl && \ rm -rf /var/lib/apt/lists/*

Брати Debian, коли Alpine працює

Більшість мовних runtime мають Alpine-варіант: node:22-alpine, python:3.13-alpine, golang:1.23-alpine. Зазвичай на 70-80% менші. Caveat: Alpine використовує musl libc, не glibc, деякі prebuilt-бінари (NumPy з Intel MKL, деякі Node native-модулі) не працюють на Alpine. Коли кусає, бери *-slim Debian-варіанти.

Забути пінитися на latest і отримати більший image випадково

node:latest може бути 1 GB; node:22-alpine 200 MB. Правильний tag це половина битви.

Реальне застосування

  • Дистрибуція статичних сайтів: фінальний стейдж nginx:alpine → 25-30 MB. Стандарт індустрії.
  • Go-сервіси: FROM scratch + бінар → 5-15 MB. Serverless-швидкі cold-старти.
  • Python ML-сервіси: python:3.13-slim + лише потрібні пакети, з --no-cache-dir усюди → 200-500 MB замість 2 GB.
  • CI-build image: одне місце, де розмір важить менше; вони живуть на runner. Але все одно 5 GB CI-image сповільнює кожен job.

Питання для поглиблення

Q: Чи компресія моїх файлів зменшує розмір image?


A: Не дуже, шари Docker уже gzip'ються при push/pull. Твоя робота на рівні файлів, не компресії.

Q: Чому мій image набагато більший за суму файлів всередині?


A: Через те, як працюють шари, файли, додані потім видалені, все одно займають місце. Бери dive або docker history, щоб знайти bloat.

Q: Чи варто брати Alpine для всього?


A: Для більшості так. Винятки: Python ML/data-science (NumPy, SciPy, pandas мають prebuilt wheels для glibc; Alpine форсує musl-compatible builds, повільно), важкі native-залежності. Для них *-slim Debian кращий дефолт.

Q: Яка різниця між distroless і Alpine?


A: Alpine має busybox, sh, apk, малий, але не мінімальний. Distroless має лише runtime, що твоїй мові потрібен (Node, Python, JVM або жоден для статики). Без shell, без package manager, без нічого. Менший і безпечніший за Alpine; важче дебажити (docker exec sh нема).

Q: (Senior) Коли агресивне зменшення розміру стає контрпродуктивним?


A: Коли дебаг у проді стає неможливим (немає shell, немає tools). Бери окремий :debug-варіант для цього. Коли складність білду злітає (10-стейджеві Dockerfile з кастомними apk-репозиторіями) для маржинального прибутку. Коли стиснутий image ламається у runtime, бо якоїсь lib не вистачає. Знайди sweet spot: достатньо малий для швидкого pull і мінімізації поверхні атаки, достатньо великий, щоб дебажити, коли треба.

Приклади

Статичний сайт: 1.2 GB → 28 MB

dockerfile
# ДО (1.2 GB) FROM node:22 WORKDIR /app COPY . . RUN npm install RUN npm run build CMD ["npx", "http-server", "dist"] # ПІСЛЯ (28 MB) FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html

Python ML-сервіс: 2.5 GB → 480 MB

dockerfile
# ПІСЛЯ FROM python:3.13-slim AS build WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt FROM python:3.13-slim WORKDIR /app COPY --from=build /install /usr/local COPY app.py . USER 1000:1000 CMD ["python", "app.py"]

Ключові ходи: slim-base, --no-cache-dir, ізольований install через prefix і copy.

Go-сервіс: 700 MB → 12 MB

dockerfile
FROM golang:1.23-alpine AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/server ./cmd/server FROM scratch COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=build /out/server /server USER 65532:65532 ENTRYPOINT ["/server"]

-ldflags="-s -w" зриває debug-символи Go-бінаря. FROM scratch нічого не додає. Бінар плюс TLS-cert bundle це увесь image.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів