core code helper

Dockerfile: інструкції, кеш шарів, multi-stage, CMD і ENTRYPOINT

Як писати Dockerfile: усі інструкції з прикладами, .dockerignore, порядок для кешу, multi-stage, CMD проти ENTRYPOINT і сигнали, healthcheck, BuildKit-кеш і секрети, готові шаблони для Python, Node і Go.

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

Перший Dockerfile

Невеликий застосунок на Python (Flask/FastAPI) — файли проєкту:

app/
├── Dockerfile
├── .dockerignore
├── requirements.txt
└── main.py
# syntax=docker/dockerfile:1
FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN useradd --create-home --uid 10001 app
USER app

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Збірка й запуск:

docker build -t myapp:1.0 .
docker run -d --name myapp -p 8000:8000 myapp:1.0

Крапка в кінці docker build — контекст збірки: папка, вміст якої відправляється демону. Лише файли з контексту можна скопіювати через COPY.

Інструкції

Інструкція Що робить Приклад
FROM базовий образ (початок етапу) FROM node:24-alpine AS build
WORKDIR робоча папка (створить, якщо немає) WORKDIR /app
COPY файли з контексту чи іншого етапу COPY --chown=app:app . .
ADD як COPY + розпаковує tar і вміє URL краще COPY, якщо магія не потрібна
RUN виконати команду під час збірки RUN apt-get update && apt-get install -y curl
ENV змінна середовища (і під час збірки, і в контейнері) ENV NODE_ENV=production
ARG змінна лише для збірки ARG VERSION=dev → docker build --build-arg VERSION=1.4.0
EXPOSE документує порт (нічого не відкриває!) EXPOSE 3000
USER від кого виконувати подальше і запускати контейнер USER node
CMD команда за замовчуванням при запуску CMD ["node", "server.js"]
ENTRYPOINT незмінна частина команди запуску ENTRYPOINT ["/entrypoint.sh"]
HEALTHCHECK як перевіряти, що сервіс живий див. нижче
LABEL метадані LABEL org.opencontainers.image.source=https://github.com/olena/notes-app
VOLUME позначає папку як точку для тому у власних образах краще не використовувати
STOPSIGNAL яким сигналом зупиняти STOPSIGNAL SIGQUIT (nginx)
RUN vs CMD

RUN виконується один раз під час збірки і результат зберігається в шарі (встановлення пакетів). CMD виконується кожного разу при старті контейнера (запуск сервера). Сплутати їх — класична помилка новачка.

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

Без нього в контекст збірки летить усе: .git, node_modules, .env із секретами, локальні бази. Збірка повільна, образ роздутий, а секрет може потрапити в образ через COPY . ..

# .dockerignore
.git
.gitignore
node_modules
.venv
__pycache__
*.pyc
dist
build
.env
.env.*
*.log
Dockerfile
compose*.yaml
.vscode
.idea
coverage

Кеш шарів і порядок інструкцій

Docker перевикористовує шар, якщо інструкція і її вхідні файли не змінилися. Щойно щось змінилося — ця інструкція і всі наступні виконуються заново.

Порівняння: якщо копіювати код до встановлення залежностей, кожна зміна коду запускає npm ci заново; якщо спершу package.json і npm ci, а код потім — збірка бере все з кешу

Правило: що змінюється рідко — вгору, що часто — вниз. Спершу системні пакети, потім маніфест залежностей і встановлення, і лише в кінці — код.

RUN: об'єднуйте й прибирайте в одному шарі

# Погано: кеш apt лишається в шарі назавжди, update може бути застарілим
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 --no-install-recommends curl ca-certificates \
 && rm -rf /var/lib/apt/lists/*

Видалення файлу в наступному RUN не зменшує образ: файл лишається в попередньому шарі.

Кеш-маунти BuildKit

Кеш пакетного менеджера між збірками, але не в образі:

# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

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

Multi-stage: збираємо окремо, запускаємо окремо

Для збірки потрібні компілятори, dev-залежності, вихідний код. Для запуску — лише результат. Multi-stage дозволяє мати кілька FROM в одному Dockerfile і копіювати між етапами лише потрібне.

Схема multi-stage: етап build з node:24 збирає dist і викидається, а фінальний образ nginx отримує лише dist через COPY --from=build

Фронтенд (React/Vite) → статичні файли в nginx:

# syntax=docker/dockerfile:1
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:stable-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

Node.js-бекенд (схоже зроблено Dockerfile цього сайту):

# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

FROM node:24-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["node", "src/server.js"]

Go → distroless (образ ~10 МБ):

# 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/server ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=build /out/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]

Зібрати лише певний етап (наприклад, для тестів): docker build --target build -t myapp:test .

CMD і ENTRYPOINT

Exec-форма і shell-форма

CMD ["node", "server.js"]     # exec-форма: node — це PID 1
CMD node server.js            # shell-форма: PID 1 — /bin/sh -c "node server.js"

Використовуйте exec-форму (JSON-масив). У shell-формі PID 1 — оболонка, яка не передає застосунку SIGTERM. docker stop чекатиме 10 секунд і вб'є процес через SIGKILL: незавершені запити обірвуться, з'єднання з БД не закриються.

Як вони поєднуються

Фінальна команда = ENTRYPOINT + CMD. CMD легко замінити аргументами docker run, ENTRYPOINT — лише через --entrypoint.

Dockerfile docker run img docker run img --help
CMD ["nginx", "-g", "daemon off;"] nginx -g daemon off; --help (замінило CMD повністю)
ENTRYPOINT ["curl"] + CMD ["--version"] curl --version curl --help
ENTRYPOINT ["/entrypoint.sh"] + CMD ["web"] /entrypoint.sh web /entrypoint.sh --help

Скрипт-обгортка

Типовий entrypoint.sh: підготувати щось (міграції, конфіг зі змінних) і передати керування основному процесу через exec:

#!/bin/sh
set -e
python manage.py migrate --noinput
exec "$@"          # замінити shell на CMD: застосунок стане PID 1 і отримуватиме сигнали
COPY --chmod=755 entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app.wsgi"]
CRLF у скриптах

Скрипт, збережений на Windows із закінченнями CRLF, у контейнері падає з exec /entrypoint.sh: no such file or directory, хоча файл є. Додайте в .gitattributes рядок *.sh text eol=lf.

HEALTHCHECK

HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
  CMD wget -qO- http://localhost:3000/healthz || exit 1

Статус видно в docker ps: Up 2 minutes (healthy). Compose використовує його в depends_on: condition: service_healthy.

Чим перевіряти

В alpine є wget (з busybox), але немає curl. У -slim образах немає ні того, ні іншого — використайте мову застосунку: CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" або node -e "fetch('http://localhost:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))".

ARG, ENV і секрети

ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION       # щоб версія була доступна застосунку в рантаймі
LABEL org.opencontainers.image.version=$APP_VERSION
docker build --build-arg APP_VERSION=1.4.0 -t myapp:1.4.0 .
Секрети не через ARG і ENV

Значення ARG і ENV видно в docker history і docker inspect кожному, хто має образ. Токен для приватного npm/pip-реєстру передавайте як секрет BuildKit — він доступний лише під час одного RUN і не потрапляє в шари:

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

Корисні прапорці docker build

docker build -t myapp:1.4.0 -t myapp:latest .   # кілька тегів одразу
docker build -f docker/Dockerfile.prod .        # інший Dockerfile
docker build --no-cache .                       # без кешу
docker build --pull .                           # перевірити свіжу версію FROM
docker build --progress=plain .                 # повний вивід команд (для налагодження)
docker build --target build .                   # лише до етапу build
docker build --platform linux/amd64 .           # під іншу архітектуру
docker init

У Docker Desktop є docker init: питає мову й фреймворк і генерує Dockerfile, compose.yaml і .dockerignore за актуальними практиками. Хороша відправна точка для нового проєкту.

Чекліст хорошого Dockerfile

Що Навіщо
конкретна версія в FROM відтворювані збірки
.dockerignore швидкий контекст, без секретів
залежності до коду кеш
multi-stage малий образ без компіляторів
USER не root безпека
exec-форма CMD коректні сигнали й швидка зупинка
логи в stdout docker logs і збирачі логів
HEALTHCHECK статус для compose і моніторингу
жодних секретів у ENV, ARG, COPY їх видно в образі
один процес на контейнер простіше перезапускати, масштабувати й логувати

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

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

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