core code helper

Dockerfile, за який не соромно: multi-stage, кеш шарів і малий образ

Як писати Dockerfile, щоб образ збирався за секунди, важив у рази менше і не тягнув у продакшен компілятори, кеші й секрети.

0 імпортували Імпортувати собі

Образ на 1.4 ГБ, який перезбирається 8 хвилин після зміни одного рядка коду, — типова картина в проєктах, де Dockerfile писали «аби запустилось». Виправити це зазвичай можна за годину, якщо розуміти, як працюють шари і кеш.

Як працює кеш шарів

Кожна інструкція (RUN, COPY, ADD) створює шар. При перезбірці Docker використовує кеш, доки інструкція і її вхідні файли не змінились. Щойно один шар змінився — усі наступні перезбираються.

# ❌ Будь-яка зміна коду інвалідовує кеш → npm ci щоразу заново
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
CMD ["node", "server.js"]
# ✅ Залежності змінюються рідко — копіюємо їх окремо і раніше
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]

Правило: що рідше змінюється — то вище в Dockerfile.

Multi-stage: збираємо в одному образі, запускаємо в іншому

Компілятори, dev-залежності й вихідники потрібні лише для збірки. У фінальний образ потрапляє тільки результат.

# syntax=docker/dockerfile:1

# --- Збірка ---
FROM golang:1.25 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/app ./cmd/api

# --- Запуск ---
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]

Результат: замість ~900 МБ (образ з Go) — 10–20 МБ, без shell і пакетного менеджера. Менше розмір — швидший деплой і менше вразливостей для сканера.

Кеш-маунти BuildKit

Навіть з правильним порядком шарів npm ci чи pip install після зміни залежностей качає все заново. Кеш-маунт зберігає кеш пакетного менеджера між збірками, не додаючи його в образ:

RUN --mount=type=cache,target=/root/.npm \
    npm ci --omit=dev

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

.dockerignore — обов'язково

Без нього в контекст збірки потрапляє все: node_modules, .git, локальні .env. Це повільно і небезпечно.

.git
node_modules
**/__pycache__
.env
.env.*
!.env.example
*.log
coverage
dist
Обережно

COPY . . без .dockerignore може запекти ваш .env з паролями прямо в образ. Навіть якщо пізніше видалити файл через RUN rm, він залишиться в попередньому шарі й буде доступний будь-кому, хто завантажить образ.

Секрети під час збірки

Якщо для збірки потрібен токен (приватний npm-реєстр, Git), не передавайте його через ARG чи ENV — вони зберігаються в історії образу. Використовуйте secret-маунти:

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci --omit=dev
docker build --secret id=npmrc,src=$HOME/.npmrc -t app .

Дрібниці, які мають значення

  • Фіксуйте версії базових образів: node:22.11-alpine замість node:latest. Для повної відтворюваності — за digest (@sha256:...).
  • Об'єднуйте команди встановлення і чистіть кеш в одному RUN:
    RUN apt-get update \
     && apt-get install -y --no-install-recommends curl ca-certificates \
     && rm -rf /var/lib/apt/lists/*
  • Не запускайте від root: USER node (є в офіційних Node-образах) або створіть свого користувача.
  • Exec-форма CMD ["node", "server.js"] замість CMD node server.js — тоді процес отримує сигнали SIGTERM і коректно завершується.
  • HEALTHCHECK для сервісів, яким це потрібно в Compose чи Swarm.

Як перевірити результат

docker images app                        # розмір
docker history app:latest                # які шари скільки важать
docker build --progress=plain .          # що бере з кешу (CACHED), а що збирає

Для глибшого аналізу шарів зручний інструмент dive: він показує, які файли додав кожен шар, і знаходить «мертву вагу».

Чому RUN rm -rf /tmp/build у окремому шарі не зменшує розмір образу?

Шари лише додаються один над одним. Видалення в новому шарі приховує файли, але вони фізично залишаються в попередньому шарі образу. Щоб файли не потрапили в образ, їх треба видаляти в тому самому RUN, де вони з'явились, або використати multi-stage збірку.

Збережіть статтю собі

Увійдіть, щоб імпортувати її у власні нотатки, відмічати вивчене, ставити лайки й питати спільноту. Реєстрація безкоштовна: вхід через Google займає кілька секунд.

Увійти через Google Інші способи входу