Як зменшити розмір Docker image?
Зменшення розміру Docker image це частково гігієна, частково архітектура. Правильні техніки на правильну проблему можуть зменшити image з 1 GB до 30 MB без втрати функціоналу. Менші image це швидші pull, швидші деплої, менша поверхня атаки.
Теорія
TL;DR
П'ять технік, у приблизному порядку impact:
- Multi-stage build з тонким фінальним base (
alpine,distroless,scratch). Найбільший single-win. - Менший base image: Debian slim → Alpine → distroless → scratch. Кожен крок ~50-100 MB менше.
- Single
RUNдля install + cleanup, щоб cache-файли не запекалися у шар. .dockerignoreдля тримання build context малим (безnode_modules,.gitтощо).- Скинь dev-deps, recommended-пакети, невикористані локалі у runtime-стейджі.
Міряй через docker images і docker history. Для глибокого аналізу dive.
Швидкий приклад: до і після
До (single stage, наївно):
FROM node:22
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]Фінал: ~1.2 GB.
Після (multi-stage, alpine, prune):
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 /app/dist /app/dist
COPY /app/node_modules /app/node_modules
USER node
CMD ["node", "dist/server.js"]Фінал: ~180 MB. Той самий функціонал.
Для статичних сайтів (Node-runtime не треба):
# Стейдж 2:
FROM nginx:1.27-alpine
COPY /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:bookworm | 120 MB | Так (bash) | apt |
debian:bookworm-slim | 75 MB | Так (bash) | apt |
ubuntu:24.04 | 80 MB | Так (bash) | apt |
alpine:3.21 | 7-8 MB | Так (sh, busybox) | apk |
gcr.io/distroless/base | 20 MB | Ні | Ні |
gcr.io/distroless/static | 2 MB | Ні | Ні |
scratch | 0 | Ні | Ні |
Обирай найменше, що має те, що твоєму бінару реально треба.
Техніка 2: об'єднуй RUN-команди і чисти cache
# НЕПРАВИЛЬНО: кожен 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 . . шле гігабайти, що не треба.
Техніка 4: скинь dev-залежності і recommended-пакети
# 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 curlDev-deps (TypeScript-компілятор, jest, eslint) часто подвоюють node_modules. --no-install-recommends ріже опційні пакети apt.
Техніка 5: мінімізуй що COPY'ється
# 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
# 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 такий, який є.
Типові помилки
Додавати файли у одному шарі, видаляти у іншому
# НЕПРАВИЛЬНО: 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 майже жоден не потрібен. Завжди:
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
# ДО (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 /app/dist /usr/share/nginx/htmlPython ML-сервіс: 2.5 GB → 480 MB
# ПІСЛЯ
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 /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
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 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY /out/server /server
USER 65532:65532
ENTRYPOINT ["/server"]-ldflags="-s -w" зриває debug-символи Go-бінаря. FROM scratch нічого не додає. Бінар плюс TLS-cert bundle це увесь image.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів