Хороший Dockerfile даёт маленький образ, быструю сборку за счёт кеша и процесс, который корректно останавливается. На собесе часто дают Dockerfile и просят найти проблемы. Ниже всё, что для этого нужно.
Основные инструкции
FROM node:22-alpine # базовый образ
WORKDIR /app # рабочий каталог (создаётся сам)
COPY package*.json ./ # копирование из контекста сборки
RUN npm ci --omit=dev # команда при сборке, создаёт слой
COPY . .
ENV NODE_ENV=production # переменная и при сборке, и в рантайме
EXPOSE 3000 # документация порта, сама порт не публикует
USER node # от кого запускать
CMD ["node", "server.js"] # команда по умолчанию
ARGзадаёт переменную только на время сборки (--build-arg),ENVсохраняется в образе. Секреты нельзя передавать ни так, ни так: они останутся в истории образа.COPYкопирует файлы.ADDумеет ещё распаковывать локальные tar-архивы и скачивать по URL; без этих причин используютCOPY.EXPOSEничего не открывает: порт публикуютdocker run -pили compose.HEALTHCHECKзадаёт проверку здоровья для Docker и compose (Kubernetes её игнорирует, там свои probes).
Кеш сборки: порядок важен
Docker кеширует каждую инструкцию. Если инструкция и её входные данные не изменились, слой берётся из кеша. Как только один слой изменился, все последующие пересобираются. Поэтому порядок такой: редко меняющееся вверх, часто меняющееся вниз.
COPY package.json package-lock.json ./
RUN npm ci
COPY . . # код меняется постоянно, но npm ci уже в кеше
Если написать COPY . . до npm ci, любая правка в коде заставит заново ставить все зависимости.
Файл .dockerignore исключает файлы из контекста сборки: node_modules, .git, .env, логи. Без него контекст огромный, кеш сбрасывается от любой мелочи, а секреты могут попасть в образ.
apt в одном RUN
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
update и install в одной инструкции, иначе кеш слоя update даст устаревшие списки. Чистка в той же инструкции, иначе она не уменьшит образ.
CMD и ENTRYPOINT
CMD: команда по умолчанию, полностью заменяется аргументамиdocker run image <cmd>.ENTRYPOINT: исполняемая часть, аргументыdocker runдописываются к ней.- Вместе:
ENTRYPOINT ["python", "manage.py"]+CMD ["runserver"], тогдаdocker run image migrateвыполнитpython manage.py migrate.
Форма важна. Exec-форма ["node", "server.js"] запускает процесс напрямую, он становится PID 1 и получает сигналы. Shell-форма node server.js запускает /bin/sh -c "node server.js": PID 1 будет shell, и SIGTERM при docker stop до приложения может не дойти. Через 10 секунд Docker пошлёт SIGKILL.
PID 1 и сигналы
У PID 1 в Linux особые правила: ядро не применяет к нему действия сигналов по умолчанию. Если приложение не поставило обработчик SIGTERM, сигнал будет проигнорирован. Кроме того, PID 1 должен забирать завершившиеся дочерние процессы, иначе копятся зомби. Решения: exec-форма и обработчик сигналов в приложении, маленький init tini (или docker run --init), в shell-скриптах-обёртках exec "$@" в конце.
Multi-stage
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/api ./cmd/api
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/api /api
USER nonroot
ENTRYPOINT ["/api"]
Первая стадия содержит компилятор и исходники, финальная только бинарник. Образ уменьшается с сотен мегабайт до десятков, а в проде нет компилятора и лишних утилит. Для Node и Python идея та же: в стадии сборки ставим инструменты и собираем, в финальную копируем результат и только продовые зависимости.
Базовые образы
debian:bookworm-slim,ubuntu: привычно, есть пакетный менеджер.alpine: очень маленький, но musl вместо glibc. Бывают проблемы с бинарными пакетами (Python-колёса, нативные модули Node) и с DNS-поведением.distroless: только рантайм и приложение, без shell и пакетного менеджера. Меньше поверхность атаки, но отладка сложнее.scratch: пустой образ, для статических бинарников.
BuildKit
BuildKit сейчас сборщик по умолчанию. Он собирает независимые стадии параллельно и умеет:
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.npm npm ci
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
Кеш-монтирование ускоряет повторные установки, а секрет доступен только во время инструкции и не попадает в слой.
Частые ошибки
COPY . .до установки зависимостей.- Нет
.dockerignore, в образ попали.gitи.env. - Shell-форма CMD, приложение не получает SIGTERM.
- Секреты через
ARG/ENVилиCOPY .env. - Запуск от root без необходимости.
- Сборочные инструменты в финальном образе вместо multi-stage.
Что спросят на собесе
- Чем CMD отличается от ENTRYPOINT, exec-форма от shell-формы.
- Как ускорить сборку за счёт кеша.
- Что такое multi-stage и зачем.
- Почему контейнер останавливается 10 секунд и как это исправить.
- Как передать секрет при сборке, не оставив его в образе.