Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як зменшити розмір Docker image?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**П'ять технік покривають більшість кейсів:** менший base image (`alpine`, `distroless`, `scratch`), multi-stage builds (скидай toolchain), single-`RUN` cleanup (видаляй cache у тому ж шарі), `.dockerignore` (малий build context), `--no-install-recommends` і `--omit=dev` (без зайвих пакетів). ```dockerfile FROM node:22-alpine AS build # ... збір артефакту ... FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html ``` **Головне:** найбільший single-win це multi-stage з тонким фінальним base. Після цього аудит кожного шару через `docker history` або `dive` і атакуй найбільший першим.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Зменшення розміру 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: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 ```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 . .` шле гігабайти, що не треба. ### Техніка 4: скинь dev-залежності і recommended-пакети ```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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.