Що таке 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 - типовий 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"]$ 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 name | Build-time змінна, ставиться через --build-arg. Відсутня у runtime (для нього ENV). |
CMD vs ENTRYPOINT
Обидва визначають що крутитися при старті container. Різниця проявляється, коли користувач робить override.
# Патерн 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. Переупорядкуй для ефективності кешу:
# НЕПРАВИЛЬНО: 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.
# Стейдж 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 /app/dist /usr/share/nginx/html
EXPOSE 80Фінальний image це nginx:1.27-alpine плюс твої зібрані статичні файли. Node toolchain, source code, node_modules, нічого з цього не потрапляє у runtime image. Менша поверхня атаки, менший image, швидший pull.
Типові помилки
Запускати під root у фінальному стейджі
# НЕПРАВИЛЬНО: дефолтний користувач root
FROM node:22
COPY . /app
CMD ["node", "app.js"]
# ПРАВИЛЬНО: скинути привілегії
FROM node:22
COPY . /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-команди неправильно
# НЕПРАВИЛЬНО: кожен 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-сервісу
# Стейдж 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 /out/server /server
EXPOSE 8080
USER 65532:65532
ENTRYPOINT ["/server"]Фінальний image приблизно розміру Go-бінаря. Без shell, без libc, без package manager. Єдине, з чим зловмисник може взаємодіяти, це твій сервіс.
Python-застосунок з cache-mount (BuildKit)
# syntax=docker/dockerfile:1.7
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt ./
RUN \
pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]Cache-mount тримає pip'ові wheels кешованими між білдами без запікання у image. Білд номер 2 з тим самим requirements.txt перевикористовує кеш; сам шар лишається чистим.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів