Skip to main content

Що таке Dockerfile?

Dockerfile це звичайний текстовий файл з інструкціями, що Docker читає зверху вниз, щоб зібрати image. Кожна нетривіальна інструкція створює новий шар; шари стекаються у фінальний image.

Теорія

TL;DR

  • Простий текст. Не JSON, не YAML. Одна інструкція на рядок у верхньому регістрі: FROM, RUN, COPY, CMD, тощо.
  • Йде зверху вниз. Раніші інструкції лягають у нижні шари; пізніші додаються поверх.
  • Кожна інструкція = один шар (кешується). Та сама інструкція з тим самим input при rebuild = cache hit, без роботи.
  • Порядок важливий: дешеві, стабільні речі (system deps) спочатку, ті, що часто міняються (твій source code), в кінці.
  • Multi-stage builds дозволяють зібрати у одному стейджі і скопіювати тільки артефакт у тонкий runtime-стейдж. Менші, безпечніші image.

Швидкий приклад

dockerfile
# Dockerfile - типовий Node.js застосунок FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . USER node EXPOSE 3000 CMD ["node", "server.js"]
bash
$ docker build -t myapp:1.0 . [+] Building 12.4s (10/10) FINISHED => [1/6] FROM node:22-alpine => [2/6] WORKDIR /app => [3/6] COPY package*.json ./ => [4/6] RUN npm ci --omit=dev => [5/6] COPY . . => [6/6] USER node => exporting layers

Сім інструкцій, шість шарів (останні EXPOSE і CMD це лише метадані). Зміни source-файл і rebuild: тільки кроки 5 і далі виконаються знову. Кроки 1-4 з кешу.

Ключові інструкції

ІнструкціяЩо робить
FROM image[:tag]Ставить базовий image. Перший рядок будь-якого Dockerfile.
WORKDIR /pathСтавить робочу директорію для наступних RUN, COPY, CMD. Створює, якщо нема.
COPY src destКопіює файли з build context у image.
ADD src destЯк COPY, але ще обробляє URL і розпаковує tar. Краще COPY, якщо не треба ці фічі.
RUN cmdЗапускає shell-команду під час збірки. Типово: встановити пакети, зібрати артефакти.
ENV KEY=valueСтавить environment-змінну, що залишається в image.
EXPOSE 80Тільки документація: «цей image слухає порт 80». Нічого реально не публікує.
USER name|uidСтавить користувача для наступних інструкцій і запущеного container. Default: root (уникай).
CMD ["prog", "arg"]Дефолтна команда, що виконується при старті container. Перевизначається через docker run.
ENTRYPOINT ["prog"]Фіксована перша частина команди; CMD стає його дефолтними args.
ARG nameBuild-time змінна, ставиться через --build-arg. Відсутня у runtime (для нього ENV).

CMD vs ENTRYPOINT

Обидва визначають що крутитися при старті container. Різниця проявляється, коли користувач робить override.

dockerfile
# Патерн A: тільки CMD CMD ["echo", "hello"] # docker run myimage -> echo hello # docker run myimage echo bye -> echo bye (CMD повністю замінений) # Патерн B: ENTRYPOINT + CMD ENTRYPOINT ["echo"] CMD ["hello"] # docker run myimage -> echo hello # docker run myimage bye -> echo bye (CMD замінено, ENTRYPOINT лишився)

Використовуй ENTRYPOINT, коли image це один інструмент (наприклад, CLI). CMD сам по собі коли image це сервіс без args.

Build cache і порядок інструкцій

Docker кешує кожен шар по інструкції + input. Переупорядкуй для ефективності кешу:

dockerfile
# НЕПРАВИЛЬНО: source копіюється до встановлення deps FROM node:22-alpine WORKDIR /app COPY . . # будь-яка зміна коду інвалідує все нижче RUN npm ci --omit=dev # запускається на кожну зміну коду CMD ["node", "server.js"] # ПРАВИЛЬНО: deps встановлено до копіювання source FROM node:22-alpine WORKDIR /app COPY package*.json ./ # міняється тільки при зміні deps RUN npm ci --omit=dev # кешовано, поки package.json не міняється COPY . . # міняється при зміні source; лише ця і нижче перезапускаються CMD ["node", "server.js"]

Різниця: rebuild після однорядкової зміни у server.js стає 1 секунду замість 60.

Multi-stage builds

Збираєш у важкому стейджі, копіюєш артефакти у тонкий стейдж. Результат: менші, безпечніші фінальні image.

dockerfile
# Стейдж 1: збірка FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # дає /app/dist # Стейдж 2: runtime FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80

Фінальний image це nginx:1.27-alpine плюс твої зібрані статичні файли. Node toolchain, source code, node_modules, нічого з цього не потрапляє у runtime image. Менша поверхня атаки, менший image, швидший pull.

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

Запускати під root у фінальному стейджі

dockerfile
# НЕПРАВИЛЬНО: дефолтний користувач root FROM node:22 COPY . /app CMD ["node", "app.js"] # ПРАВИЛЬНО: скинути привілегії FROM node:22 COPY --chown=node:node . /app USER node CMD ["node", "app.js"]

Root-container, що вирвався з namespace, все ще root на хості. Завжди перемикайся на non-root перед CMD.

Не використовувати .dockerignore

# .dockerignore node_modules .git dist *.log .env* Dockerfile

Без нього COPY . . шле твої node_modules і .git на daemon, що сповільнює білди і роздуває image.

Об'єднувати непов'язані RUN-команди неправильно

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

Якщо ти видаляєш файли у пізнішому шарі, попередній шар все ще їх містить, видалення лише ховає. Чисти в тому самому RUN, що створив сміття.

Використовувати ADD, де COPY достатньо

ADD розпаковує tarball'и і тягне URL. Обидві поведінки людей дивують. Використовуй COPY для звичайного копіювання; тягнися за ADD тільки коли тобі реально потрібні ці фічі.

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

  • CI/CD пайплайни: кожен PR тригерить docker build проти Dockerfile репо. Cache hit rate 80-90 відсотків на добре впорядкованих Dockerfile тримає білди швидкими.
  • Multi-arch білди: docker buildx build --platform linux/amd64,linux/arm64 -t myapp:1.0 . дає multi-platform image з одного Dockerfile. Використовується, коли той самий застосунок деплоїться на x86 сервери і ARM (Mac M-серії, Graviton).
  • Distroless / scratch images: FROM gcr.io/distroless/base або FROM scratch для фінального стейджу multi-stage. Фінальний image містить тільки твій бінар, без shell, без package manager, без поверхні атаки поза самим застосунком.
  • BuildKit-фічі: # syntax=docker/dockerfile:1.7 зверху відкриває фічі типу RUN --mount=type=cache,target=/root/.npm для персистентного npm-кешу між білдами.

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

Q: Яка різниця між RUN, CMD і ENTRYPOINT?


A: RUN виконується під час збірки і запікає результат у шар. CMD і ENTRYPOINT виконуються при старті container і визначають дефолтний процес. Збірка vs запуск, ось межа.

Q: Чому мої білди весь час перетягують залежності?


A: Скоріш за все, бо ти робиш COPY . . перед встановленням deps. Будь-яка зміна будь-якого файлу інвалідує кеш для цього рядка і всього після, включно з install-кроком. Підніми install вище: спочатку lock-файли, install, потім решта.

Q: Що таке BuildKit і чи мені він треба?


A: BuildKit це сучасний build-engine Docker (дефолт з Docker 23). Дозволяє паралельні стейджі, cache-mount'и, secret-mount'и і директиву # syntax=docker/dockerfile:1.x, що додає нові інструкції. Майже завжди він уже у тебе. Запусти docker buildx version, щоб переконатися.

Q: Коли використовувати ARG vs ENV?


A: ARG для build-time-only значень (наприклад, --build-arg VERSION=1.2.3 для тегування білду). ENV для runtime-значень, що мають бути видимі всередині запущеного container (наприклад, ENV NODE_ENV=production). ARG зникає після білду; ENV залишається.

Q: (Senior) Як обробляти secret'и під час збірки, не зливаючи їх у шар?


A: Використовуй BuildKit secret-mount'и: RUN --mount=type=secret,id=npmrc cp /run/secrets/npmrc ~/.npmrc && npm ci. Secret доступний для RUN-кроку, але ніколи не пишеться у шар. Передавай через docker buildx build --secret id=npmrc,src=$HOME/.npmrc .. Build-args (ARG) зливаються у image history і не повинні нести secret'и.

Приклади

Multi-stage білд для Go-сервісу

dockerfile
# Стейдж 1: збірка 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 -o /out/server ./cmd/server # Стейдж 2: runtime, нічого крім бінаря FROM scratch COPY --from=build /out/server /server EXPOSE 8080 USER 65532:65532 ENTRYPOINT ["/server"]

Фінальний image приблизно розміру Go-бінаря. Без shell, без libc, без package manager. Єдине, з чим зловмисник може взаємодіяти, це твій сервіс.

Python-застосунок з cache-mount (BuildKit)

dockerfile
# syntax=docker/dockerfile:1.7 FROM python:3.13-slim WORKDIR /app COPY requirements.txt ./ RUN --mount=type=cache,target=/root/.cache/pip \ pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "app.py"]

Cache-mount тримає pip'ові wheels кешованими між білдами без запікання у image. Білд номер 2 з тим самим requirements.txt перевикористовує кеш; сам шар лишається чистим.

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

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

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

Коментарі

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