Dockerfile: інструкції, кеш шарів, multi-stage, CMD і ENTRYPOINT
Як писати Dockerfile: усі інструкції з прикладами, .dockerignore, порядок для кешу, multi-stage, CMD проти ENTRYPOINT і сигнали, healthcheck, BuildKit-кеш і секрети, готові шаблони для Python, Node і Go.
Перший 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 перевикористовує шар, якщо інструкція і її вхідні файли не змінилися. Щойно щось змінилося — ця інструкція і всі наступні виконуються заново.
Правило: що змінюється рідко — вгору, що часто — вниз. Спершу системні пакети, потім маніфест залежностей і встановлення, і лише в кінці — код.
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 ciMulti-stage: збираємо окремо, запускаємо окремо
Для збірки потрібні компілятори, dev-залежності, вихідний код. Для запуску — лише результат. Multi-stage дозволяє мати кілька FROM в одному Dockerfile і копіювати між етапами лише потрібне.
Фронтенд (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 80Node.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_VERSIONdocker 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 cidocker 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 Інші способи входу