
Dockerfile: как собрать воспроизводимый и компактный образ
Создаём Dockerfile для небольшого приложения: ограничиваем build context, используем слои и multi-stage, запускаем без root и проверяем готовый образ.
Содержание
Dockerfile — текстовый файл с последовательностью шагов сборки образа. Он заменяет ручную установку пакетов внутри уже запущенного контейнера: исходный код и список зависимостей становятся входными данными, а готовый образ — проверяемым результатом. Это позволяет собрать приложение заново на другом компьютере и увидеть историю изменения каждого шага.
Из чего получится готовый образ
Начните с минимального доверенного base image, передавайте builder только нужные файлы через .dockerignore, копируйте lock-файл до исходного кода и используйте multi-stage build. Финальный процесс запускайте не от root, а результат проверяйте запуском, health endpoint и просмотром метаданных.
Подготовьте пример приложения
Пример использует Node.js только для демонстрации структуры. Все пять файлов создаются в новом каталоге example-app на рабочем компьютере. Те же принципы применимы к Go, Python, Java и статическим сборкам.
example-app/
├── package.json
├── package-lock.json
├── server.js
├── Dockerfile
└── .dockerignore
Файл server.js содержит веб-сервер с двумя ответами. Путь /health возвращает ok, чтобы после сборки можно было проверить не только запуск процесса, но и HTTP-ответ приложения:
import http from 'node:http';
const port = Number(process.env.PORT || 3000);
const server = http.createServer((request, response) => {
response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
response.end(request.url === '/health' ? 'ok\n' : 'example\n');
});
server.listen(port, '0.0.0.0');
Переменная PORT позволяет контейнеру получить номер порта при запуске; если её нет, сервер выбирает 3000. Адрес 0.0.0.0 нужен, чтобы запрос дошёл до процесса через сеть контейнера. В файле нет состояния и паролей: секреты для работающей программы передаются при запуске и не становятся частью образа.
Не передавайте сборщику лишние файлы
Команда docker build передаёт сборщику каталог, указанный последним аргументом. Этот набор называется контекстом сборки. Чем он шире, тем больше лишних данных можно случайно включить и тем чаще Docker теряет пригодный результат предыдущего шага.
.dockerignore:
.git
.env
.env.*
node_modules
npm-debug.log*
coverage
dist
README.md
Проверьте список самостоятельно: если приложению во время build нужен README или готовый dist, не исключайте его бездумно. Главное — явно запретить Git-историю, локальные зависимости, секреты и результаты предыдущей сборки.
Отделите установку зависимостей от запуска
Multi-stage build состоит из нескольких стадий FROM. В первой устанавливаются зависимости, а во вторую копируется только то, что нужно запущенному приложению. Создайте файл Dockerfile в корне example-app:
# 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 AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=node:node package.json server.js ./
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Версию base image выбирайте по поддержке приложения и официальной документации издателя. Тег в примере со временем может указывать на обновлённый образ; для точного релиза сохраните digest и обновляйте его через review.
Почему порядок инструкций важен
Сначала копируются package.json и lock-файл, затем устанавливаются зависимости. Изменение server.js не заставляет npm работать заново, пока описание зависимостей не поменялось.
Плохой порядок:
COPY . .
RUN npm ci --omit=dev
Любая правка исходника инвалидирует слой установки. Кроме того, состав COPY . . трудно оценить без строгого .dockerignore.
Объединяйте связанные операции пакетного менеджера в одном слое. Для APT обновление индекса и установка должны выполняться одной инструкцией, а кеш списков — удаляться в том же слое. Иначе builder способен переиспользовать устаревший индекс.
Соберите образ и проверьте его настройки
Откройте терминал в каталоге example-app. Первая команда соберёт образ и присвоит ему локальный тег, следующие три покажут размер, историю слоёв, пользователя и команду запуска:
docker build --pull -t example-app:local .
docker image ls example-app:local
docker image history example-app:local
docker image inspect example-app:local \
--format 'user={{.Config.User}} cmd={{json .Config.Cmd}} size={{.Size}}'
Точка в конце первой команды означает текущий каталог — тот самый контекст сборки. --pull проверяет обновление базового образа, но не отключает кеш сборки. --no-cache заставляет повторить стадии, однако сам по себе не скачивает свежую основу. Эти параметры решают разные задачи.
В метаданных ожидаются непривилегированный пользователь и exec-form команды. JSON-массив в CMD передаёт сигналы непосредственно процессу, без лишнего shell.
Запустите проверяемый контейнер
docker run -d --name example-app-test \
--read-only \
--tmpfs /tmp \
-p 127.0.0.1:3000:3000 \
example-app:local
curl --fail --silent --show-error http://127.0.0.1:3000/health
docker logs --tail 50 example-app-test
docker inspect --format 'status={{.State.Status}}' example-app-test
--read-only подходит только приложению, которое не записывает в root filesystem. Временный /tmp предоставлен отдельно. Если программа требует каталог данных, подключите точно определённый volume и настройте владельца, а не возвращайте root.
После теста:
docker rm -f example-app-test
Не прячьте ошибки установки
Конструкции вроде RUN command || true делают зелёную сборку даже при отказе важного шага. Builder должен остановиться, если зависимость не установилась, тест не прошёл или обязательный файл отсутствует.
Также избегайте:
latestбез документированной политики обновления;- загрузки исполняемого файла без проверки источника и checksum;
- секретов в
ARG,ENV,COPYи URL; - установки отладочных утилит в production stage;
- запуска daemon или нескольких несвязанных служб внутри одного контейнера;
- ручного изменения готового контейнера вместо новой сборки.
Для приватных зависимостей применяйте BuildKit secret mounts, если они действительно нужны. Секрет должен существовать только во время конкретной инструкции и не попадать в слой или build log.
Воспроизводимость и обновления
Полная воспроизводимость требует контроля всех входов:
- base image по digest;
- lock-файл зависимостей;
- проверенные внешние артефакты и checksums;
- версия builder и целевая платформа;
- отсутствие текущего времени и случайных сетевых данных в результате.
При этом зафиксированный образ стареет. Регулярно собирайте новый кандидат с обновлённой основой и зависимостями, прогоняйте тесты и явно меняйте digest. Контрольная история обновлений безопаснее и понятнее, чем tag, который перемещается без review.
Критерии готового Dockerfile
Готовый образ собирается из чистого checkout, не содержит .git и .env, запускается непривилегированным пользователем, корректно принимает stop-сигнал и проходит HTTP-проверку. Его точная ссылка сохраняется для deploy, а предыдущая остаётся доступной для rollback.
Правила выбора основы описаны в материале о тегах и digest. Проверять запущенный экземпляр нужно через run, logs и inspect, а значения окружения — хранить вне образа и Git.
Источники
Самопроверка
Проверьте, что материал усвоен
Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.
Разбираем коротко
Частые вопросы
Почему COPY . . часто ухудшает Docker-сборку?
Команда копирует весь build context, включая ненужные файлы, если их не исключил .dockerignore. Любое изменение также рано инвалидирует кеш последующих слоёв.
Зачем использовать multi-stage build?
Сборочные инструменты и исходники остаются в промежуточной стадии, а финальный образ получает только runtime-файлы. Это уменьшает размер и поверхность атаки.
Гарантирует ли одинаковый Dockerfile одинаковый образ?
Нет. Mutable base tag, незакреплённые зависимости, сетевые репозитории и меняющиеся даты способны изменить результат. Для воспроизводимости фиксируйте входы и контролируемо обновляйте их.


