Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке Dockerfile?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Dockerfile** це звичайний текстовий файл з build-інструкціями, що Docker читає зверху вниз, щоб зібрати image. Кожна інструкція додає шар. ```dockerfile FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . USER node CMD ["node", "server.js"] ``` ```bash $ docker build -t myapp:1.0 . ``` **Головне:** Dockerfile це рецепт, `docker build` його виконує, результат це image. Порядок важливий, дешеві стабільні шари спочатку, ті, що часто міняються, в кінці, щоб кеш перевикористовувався при rebuild.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 name` | Build-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` перевикористовує кеш; сам шар лишається чистим.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.