Быстрый старт: как собрать Docker-образ из Dockerfile
Для сборки нужен установленный Docker Engine или Docker Desktop, каталог проекта и исходный код приложения. Минимальная последовательность выглядит так: создать Dockerfile, передать его каталог в качестве build context, собрать образ с тегом, запустить контейнер и проверить логи.
docker build -t app:local .
docker images app:local
docker run --name app-local -d -p 8080:8080 app:local
docker ps
docker logs app-local
Точка в конце команды docker build -t app:local . обозначает текущий каталог. Docker отправляет его содержимое builder-процессу и использует файлы из него для выполнения инструкций COPY и ADD. Если Dockerfile лежит в другом месте, путь к нему задают через -f, но build context все равно указывают отдельно.
Сборка завершилась успешно, когда Docker создал образ и присвоил ему тег. Это еще не подтверждает запуск приложения: контейнер может завершиться из-за неправильной команды, отсутствующего файла, закрытого порта или ошибки конфигурации. Проверяйте результат через docker ps, docker logs и smoke-тест.
Подготовить каталог проекта и build context
Создайте отдельный каталог для примера. В нем будут Dockerfile, исходный код и файл, который исключает ненужные данные из контекста:
app-project/
├── Dockerfile
├── app.py
└── .dockerignore
Простейшее приложение на Python может отвечать на HTTP-запросы без сторонних зависимостей:
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = b'container is running\n'
self.send_response(200)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(('0.0.0.0', 8080), Handler).serve_forever()
Docker видит только файлы, попавшие в переданный context. Команда из каталога app-project использует текущую директорию:
cd app-project
docker build -t app:local .
Попытка выполнить COPY ../config.yaml /app/ не даст прочитать файл выше context. Docker остановит сборку, потому что такой путь находится за пределами разрешенного набора файлов.
Создать минимальный Dockerfile
Для примера используйте такой Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY app.py .
EXPOSE 8080
CMD ["python", "app.py"]
FROM выбирает базовый образ. В примере это минимальный вариант Python на базе Debian. WORKDIR задает рабочий каталог для следующих инструкций и процесса контейнера. COPY переносит файл из build context в образ. EXPOSE сообщает ожидаемый порт приложения, но сам по себе не публикует его на хосте.
CMD задает команду по умолчанию при запуске контейнера. Она не выполняется во время сборки. Инструкции FROM, WORKDIR, COPY и EXPOSE формируют образ и его метаданные, а CMD описывает поведение уже запущенного контейнера.
Для приложений, которым нужен принудительный процесс запуска, используют ENTRYPOINT. Например, ENTRYPOINT ["/app/server"] фиксирует исполняемый файл, а CMD ["--config", "/app/config.yaml"] задает аргументы по умолчанию. Изменить команду запуска можно через аргументы docker run.
Собрать образ командой docker build
Базовая команда сборки:
docker build -t app:local .
Ключ -t задает имя и тег. В записи app:local имя образа равно app, а тег local обозначает локальную проверку. Если тег не указать, Docker использует значение latest, которое плохо подходит для контроля версий.
Явный путь к Dockerfile нужен, когда файл имеет другое имя или лежит в отдельном каталоге:
docker build -f docker/Dockerfile -t app:local .
В этой команде docker/Dockerfile задает файл инструкций, а точка остается build context. Если приложению нужны параметры, передайте их через --build-arg:
docker build --build-arg APP_VERSION=1.4.0 -t app:1.4.0 .
В Dockerfile такой параметр объявляют инструкцией ARG APP_VERSION. Не передавайте через ARG токены, пароли и приватные ключи: значения могут попасть в историю сборки или логи.
При диагностике полезен подробный вывод BuildKit:
docker build --progress=plain -t app:debug .
В логе видны отдельные шаги, признаки использования кэша и точная команда, на которой произошел сбой.
Запустить контейнер и проверить первый результат
Запустите контейнер в фоне и сопоставьте порт контейнера с портом хоста:
docker run --name app-local -d -p 8080:8080 app:local
Первое значение в -p 8080:8080 относится к хосту, второе к порту внутри контейнера. После запуска проверьте состояние:
docker ps
docker port app-local
docker logs app-local
Для остановки и удаления контейнера используйте:
docker stop app-local
docker rm app-local
Если процесс завершился сразу, контейнер не появится в обычном выводе docker ps. Посмотрите все контейнеры и код завершения:
docker ps -a
docker inspect app-local --format '{{.State.ExitCode}}'
docker logs app-local
Порт, заданный через EXPOSE, не открывает доступ к приложению автоматически. Доступ появляется после публикации через -p или подключения контейнера к нужной Docker-сети.
Как устроены Dockerfile, слои и build context
Dockerfile описывает последовательность действий, по которой builder создает образ. Итог состоит из базового образа, слоев с изменениями файловой системы и метаданных запуска. Каждый слой может использоваться повторно в другой сборке, если Docker находит подходящий кэш.
Слои влияют на размер образа, скорость повторной сборки и объем данных, который нужно передать в registry. Подробный разбор состава слоев и команд анализа есть в руководстве по анализу слоев Docker-образов.
Какие инструкции создают изменения в образе
| Инструкция | Назначение | Практический риск |
|---|---|---|
FROM | Выбирает базовый образ и начинает новую стадию. | Неподходящая архитектура или устаревшая версия. |
RUN | Выполняет команду при сборке, например устанавливает пакеты. | Большой слой, незакрытый кэш пакетов, нестабильная команда. |
COPY | Переносит файлы из build context в образ. | Случайное попадание исходников, секретов и локальных кэшей. |
ADD | Копирует файлы и поддерживает дополнительные сценарии, включая распаковку локальных архивов. | Менее очевидное поведение. Для обычного копирования чаще подходит COPY. |
ENV | Задает переменную окружения в образе и контейнере. | Секреты сохраняются в метаданных образа. |
ARG | Передает параметр только на этапе сборки. | Значение может попасть в историю и не подходит для секретов. |
USER | Выбирает пользователя для следующих инструкций и запуска. | Процесс не сможет писать в нужный каталог без корректных прав. |
EXPOSE | Документирует порт приложения. | Иногда его ошибочно принимают за публикацию порта. |
CMD | Задает команду по умолчанию при запуске. | Команда может быть заменена аргументами docker run. |
ENTRYPOINT | Фиксирует основной исполняемый процесс контейнера. | Неправильная форма усложняет передачу аргументов и остановку процесса. |
RUN, COPY и ADD обычно добавляют изменения в файловую систему и участвуют в цепочке кэша. ENV, USER, EXPOSE, CMD и ENTRYPOINT меняют конфигурацию и метаданные. Точное поведение зависит от builder и версии Dockerfile frontend, поэтому при диагностике анализируйте итог через docker history и docker image inspect.
Почему порядок инструкций имеет значение
Расположите редко изменяющиеся шаги выше часто изменяющихся. Для приложения с зависимостями типичный порядок такой:
FROM базовый-образ
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Изменение app.py инвалидирует слой после COPY . ., но установка зависимостей может остаться в кэше. Если скопировать весь проект перед RUN pip install, любое изменение исходного кода заставит Docker повторно скачивать и устанавливать пакеты.
Манифесты и lock-файлы копируйте отдельной инструкцией. Для Python это может быть requirements.txt или несколько файлов зависимостей, для Node.js, package.json и package-lock.json, для Go, go.mod и go.sum. Состав пакетов должен зависеть от зафиксированного файла, а не от случайного состояния registry.
Build context и файл .dockerignore
Файл .dockerignore уменьшает объем данных, которые Docker обрабатывает перед сборкой:
.git
.gitignore
node_modules
.venv
__pycache__
*.pyc
*.log
.env
.env.*
dist
build
coverage
Dockerfile*
Список корректируют под проект. Нельзя исключать файл, который нужен инструкции COPY. Если в Dockerfile написано COPY package.json package-lock.json ./, оба файла должны попасть в context.
.dockerignore влияет на контекст и скорость передачи, но не удаляет файл из уже созданного слоя. Если секрет попал в ранний слой, последующая инструкция RUN rm /app/.env не гарантирует его исчезновение из истории образа. Такой образ нужно считать скомпрометированным, удалить секрет и выпустить новый ключ.
Кэширование Docker build: как ускорить повторные сборки
Docker проверяет, можно ли заменить очередной шаг результатом из кэша. При совпадении инструкции и входных данных builder пропускает выполнение. После первого несовпадения последующие зависимые шаги обычно выполняются заново, поэтому порядок Dockerfile напрямую влияет на время CI/CD-сборки.
Как Docker определяет, можно ли использовать кэш
Для инструкции COPY Docker учитывает содержимое передаваемых файлов и параметры операции. Изменение файла, его списка или текста инструкции может инвалидировать соответствующий слой. Для RUN важен текст команды и состояние предыдущего слоя. Docker не обязан самостоятельно проверять, изменились ли внешние файлы, доступные по той же команде.
Например, слой с командой RUN apt-get update способен взяться из кэша даже после появления новых пакетов в репозитории, если сама команда и предыдущие слои не изменились. Для контролируемого обновления применяют --pull, меняют lock-файл или намеренно запускают сборку без кэша.
В логе BuildKit строка CACHED показывает повторное использование результата. Если после изменения исходников Docker пересобирает установку зависимостей, ищите ранний COPY . ., изменяемый файл конфигурации или нестабильное правило исключений.
Разместить стабильные и изменяемые шаги в правильном порядке
Шаблон для Python-проекта с lock-подобным списком зависимостей:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
CMD ["python", "src/app.py"]
При изменении файлов в src/ слой с requirements.txt сохраняется. При изменении самого списка зависимостей Docker пересобирает установку, что соответствует ожидаемому поведению.
Для Node.js используйте манифесты отдельно:
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]
npm ci устанавливает версии из lock-файла и завершает работу при рассинхронизации манифеста. Команда npm install может изменить lock-файл во время сборки, поэтому для CI/CD чаще выбирают npm ci.
Кэширование пакетных менеджеров через BuildKit
BuildKit умеет монтировать отдельный каталог кэша на время выполнения команды. Такой кэш сохраняется между сборками builder, но его содержимое не записывается в слой runtime-образа:
# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Для Go можно хранить модульный и компиляционный кэш:
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build go mod download
Каталог кэша ускоряет загрузку уже скачанных пакетов, но не фиксирует их версии. Lock-файл и контроль версий registry остаются обязательными.
Если команда RUN --mount не распознается, проверьте, что сборка идет через BuildKit и актуальный Docker builder:
DOCKER_BUILDKIT=1 docker build -t app:local .
docker buildx ls
Управление кэшем: --no-cache, --pull, cache-from и cache-to
--no-cache запускает инструкции заново и помогает проверить, не скрывает ли кэш ошибку или устаревший пакет:
docker build --no-cache -t app:clean .
--pull просит проверить более свежую версию базового образа:
docker build --pull -t app:updated .
Обычную разработческую сборку не нужно каждый раз запускать с этими флагами. Иначе теряется преимущество кэша, а результат может отличаться от предыдущей проверки.
В эфемерном CI-раннере локального кэша после job обычно нет. BuildKit может сохранять его в registry:
docker buildx build --cache-from=type=registry,ref=registry.company.local/team/app:buildcache --cache-to=type=registry,ref=registry.company.local/team/app:buildcache,mode=max -t registry.company.local/team/app:commit --push .
cache-from указывает источник, а cache-to сохраняет новые результаты. Режим mode=max сохраняет больше промежуточных слоев и часто полезен для multi-stage build. Учетные данные, права записи и доступ runner к registry нужно проверить отдельно.
Multi-stage build Docker: как отделить сборку от финального образа
Multi-stage build разделяет инструменты сборки и runtime. Компиляторы, исходный код, тестовые зависимости и кэш остаются в стадии builder. В финальный образ копируется бинарник или подготовленный каталог приложения.
Такой подход сокращает размер runtime-образа и количество пакетов, доступных процессу во время работы. Практические шаблоны для Python, Node.js и Go собраны в руководстве по многоэтапной сборке в Docker.
Структура multi-stage build в Dockerfile
Каждый новый FROM начинает отдельную стадию. Имя после AS позволяет обратиться к ней из последующих стадий:
FROM golang:1.23 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/app
FROM alpine:3.20
RUN apk add --no-cache ca-certificates && adduser -D -H appuser
WORKDIR /app
COPY --from=builder /out/app /app/app
USER appuser
EXPOSE 8080
ENTRYPOINT ["/app/app"]
Стадия builder содержит Go SDK и исходный код. Runtime-стадия начинается с Alpine, создает сертификаты и пользователя, затем переносит один бинарник через COPY --from=builder. Если target не задан явно, итоговым считается последний этап Dockerfile.
Промежуточные стадии не попадают в образ, который строится от последнего FROM. Их слои могут оставаться в кэше builder, но при публикации runtime-образ содержит собственную цепочку слоев.
Пример multi-stage build для приложения со сборкой
Полный сценарий состоит из четырех действий: скопировать манифесты, скачать зависимости, собрать артефакт, перенести в runtime только нужные файлы. Кэш BuildKit можно добавить на шаг загрузки и компиляции:
# syntax=docker/dockerfile:1
FROM golang:1.23 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/app
FROM alpine:3.20 AS runtime
RUN apk add --no-cache ca-certificates && adduser -D -H appuser
WORKDIR /app
COPY --from=builder /out/app ./app
COPY --from=builder /src/config/production.yaml ./config/production.yaml
USER appuser
EXPOSE 8080
ENTRYPOINT ["/app/app"]
В реальном проекте путь к бинарнику и конфигурации зависит от структуры репозитория. Секреты не копируйте из builder. Передавайте их при запуске через механизм секретов оркестратора или секретное хранилище.
Для Java в runtime обычно переносят собранный JAR, для Node.js, каталог production-зависимостей и собранные статические файлы, для Python, виртуальное окружение или установленные production-пакеты. Runtime-библиотеки должны присутствовать в финальной стадии, даже если их не было в builder.
Что проверить после перехода на multi-stage build
- Для динамического бинарника проверьте библиотеки через
ldd. Статический Go-бинарник сCGO_ENABLED=0обычно не требует glibc, но это нужно подтвердить тестом. - Проверьте CA-сертификаты, если приложение обращается к TLS-сервисам. В минимальном образе каталога сертификатов может не быть.
- Проверьте рабочий каталог, права пользователя и возможность записи в нужные каталоги.
- Сверьте переменные окружения, порт, команду запуска и обработку сигналов завершения.
- Запустите контейнер на той же архитектуре, где он будет работать в production.
Builder и runtime могут использовать разные семейства Linux. Собранный бинарник должен поддерживать библиотеки финального образа. Слишком маленький runtime без shell удобен для эксплуатации, но усложняет диагностику, поэтому команды проверки лучше выполнить до публикации.
Как уменьшить размер Docker-образа
Размер образа складывается из базового слоя и добавленных файлов. Замена базового образа дает результат только тогда, когда приложение совместимо с его библиотеками. Практический порядок действий: уменьшить build context, убрать dev-инструменты из runtime, применить multi-stage build, очистить временные данные и измерить каждый результат.
Выбрать подходящий базовый образ
Полноразмерный образ обычно содержит больше системных утилит и библиотек, поэтому он удобнее для диагностики, но тяжелее. Вариант slim убирает часть пакетов и сохраняет привычную совместимость Debian или Ubuntu. Alpine занимает меньше места, но использует musl libc, что может вызвать проблемы с бинарными пакетами, Python-модулями и нативными расширениями.
Для production выбирайте самый маленький образ, который проходит функциональные тесты и содержит нужные библиотеки. Не заменяйте рабочий Debian-based runtime на Alpine без проверки запуска, TLS, DNS, локалей и нативных зависимостей.
Тег образа нужно фиксировать осознанно. Запись python:3.12-slim удобна для обновлений, но со временем может указывать на другой digest. Для строгой повторяемости применяют запись вида python:3.12-slim@sha256:значение-digest. Это требует отдельного процесса обновления и проверки базовых образов.
Исключить лишние файлы через .dockerignore
В context часто случайно попадают каталог .git, node_modules, виртуальные окружения, логи, результаты тестов, документация, локальные конфигурации и секреты. Они увеличивают передачу данных и могут раскрыть сведения о рабочей станции.
.git
node_modules
.venv
.env
.env.*
*.log
coverage
tmp
dist
build
.cache
.vscode
.idea
Исключение из context не равно удалению из слоя. Если файл требуется во время сборки, используйте его только на подходящем этапе и не переносите в runtime. Если конфигурация нужна приложению при запуске, передавайте ее через переменные окружения, секреты или подключаемый конфигурационный файл.
Не переносить dev-зависимости и инструменты сборки в runtime
Компиляторы, заголовочные файлы, тестовые фреймворки, линтеры и исходный код нужны builder. В runtime остаются исполняемый файл, production-зависимости, сертификаты, конфигурация без секретов и пользователь запуска.
Multi-stage build решает задачу через явный список артефактов:
COPY --from=builder /out/app /app/app
COPY --from=builder /out/static /app/static
Широкая команда COPY --from=builder /src /app переносит в финальный образ больше данных и стирает смысл разделения стадий. Перечисляйте конкретные каталоги и файлы.
Убирать временные файлы в том же слое
Удаление файла в следующем RUN не уменьшает слой, где этот файл появился. Установку пакетов и очистку кэша выполняйте одной командой:
RUN apt-get update && apt-get install -y --no-install-recommends build-essential ca-certificates && make build && rm -rf /var/lib/apt/lists/*
Для Alpine используют apk add --no-cache, который не сохраняет индекс пакетов в обычном виде:
RUN apk add --no-cache ca-certificates
Установка инструментов в builder все равно предпочтительнее для runtime-образа. Очистка уменьшает промежуточную стадию и полезна для одноэтапного Dockerfile, но не заменяет разделение сборки и запуска.
Измерить эффект оптимизации
Сначала зафиксируйте исходный результат, затем меняйте одну группу инструкций и сравнивайте показатели:
docker images app:local
docker image inspect app:local --format '{{.Size}}'
docker history --human app:local
docker images показывает размер образа в удобном виде. docker image inspect возвращает размер в байтах и позволяет получать точные значения в скриптах. docker history помогает найти слой, созданный большой командой RUN или широким COPY.
Для визуального анализа можно использовать инструмент dive. Он показывает файлы по слоям, удаленные данные и места, где образ получил лишний вес. Сравнивайте размер сжатого образа в registry и размер распакованных слоев локального Docker Engine: эти значения отличаются.
Проверка Docker-образа после сборки
Проверка должна подтверждать четыре свойства: собран правильный тег, в образе есть нужные файлы, процесс запускается с ожидаемыми параметрами, приложение отвечает в чистом контейнере без помощи локального каталога.
Проверить метаданные и слои образа
Получите основные настройки образа:
docker image inspect app:local --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}} Env={{json .Config.Env}} Workdir={{.Config.WorkingDir}} Ports={{json .Config.ExposedPorts}}'
Проверьте отдельно размер и историю инструкций:
docker image inspect app:local --format '{{.Size}}'
docker history --no-trunc app:local
В истории ищите крупные слои, команды с загрузкой архивов и неожиданные копирования. Наличие удаленного файла в текущей файловой системе не доказывает, что его содержимое отсутствует в предыдущем слое.
Выполнить smoke-тест контейнера
Запустите контейнер с предсказуемым именем и проверьте опубликованный порт:
docker run --name app-smoke -d -p 18080:8080 app:local
docker ps --filter name=app-smoke
docker port app-smoke
Для приложения из первого примера проверку можно выполнить через Python внутри контейнера:
docker exec app-smoke python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8080/').status)"
Ожидаемый код ответа равен 200. Для реального сервиса используйте endpoint проверки здоровья, который не требует тяжелой бизнес-операции. После теста проверьте логи и удалите контейнер:
docker logs app-smoke
docker stop app-smoke
docker rm app-smoke
В Dockerfile можно задать автоматическую проверку:
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/')"
Статус смотрят через docker inspect. Healthcheck не исправляет приложение и не заменяет smoke-тест, он сообщает Docker о состоянии процесса по заданному условию.
Проверить поведение без исходного каталога и локальных зависимостей
Не запускайте финальную проверку с bind mount исходников:
docker run -v $(pwd):/app app:local
Такой mount может скрыть файлы, скопированные в образ, и создать ложный успех. Для чистого теста используйте только image reference:
docker run --rm app:local
Проверьте, что контейнер не зависит от локального .venv, каталога node_modules, конфигурации хоста или случайно доступного UNIX-сокета Docker. В CI smoke-тест должен запускаться в отдельном контейнере после сборки.
Типичные ошибки при сборке локально и в CI/CD
Dockerfile или файлы проекта не найдены
Ошибка failed to read dockerfile обычно связана с текущим каталогом или неверным именем файла. Проверьте путь:
pwd
ls -la
ls -la docker
docker build -f docker/Dockerfile -t app:local .
Ошибка COPY failed: file not found in build context означает, что файл отсутствует в context или исключен через .dockerignore. Сначала проверьте, что файл находится под каталогом, переданным последним аргументом docker build. Затем временно проанализируйте правила ignore и имя файла с учетом регистра.
Не удается скачать базовый образ из Docker Hub
Сборка может остановиться на FROM, еще до выполнения остальных инструкций. Проверьте имя и тег, авторизацию и сетевой доступ:
docker pull python:3.12-slim
docker login
docker info
Ошибки 401 Unauthorized и 429 Too Many Requests обычно связаны с учетной записью или лимитами registry. Ошибка 403 Forbidden может указывать на ограничения доступа из конкретной сети. Для подключений из отдельных сетей, включая возможные ограничения доступа из РФ, Docker Hub может быть недоступен или возвращать 403 Forbidden, из-за чего срываются docker pull и CI/CD-сборки.
В рабочей инфраструктуре проверьте корпоративный registry, разрешенный mirror или прокси, заданный администраторами. Учитывайте политики безопасности и лицензирования. Не подменяйте официальный источник случайным образом: зафиксируйте происхождение базового образа и его digest.
Ошибка несовместимости архитектуры платформы
Образ может собраться на amd64, но не запуститься на arm64, либо наоборот. Узнайте архитектуру Docker Engine:
docker info --format '{{.OSType}}/{{.Architecture}}'
docker version
Для целевой платформы можно явно выбрать базовый образ:
docker buildx build --platform linux/amd64 -t app:amd64 .
Для публикации multi-platform image builder собирает варианты отдельно:
docker buildx build --platform linux/amd64,linux/arm64 -t registry.company.local/team/app:release --push .
Кросс-сборка не отменяет тестирование. Запустите результат на каждой целевой архитектуре или используйте runner соответствующего типа. Особое внимание уделите нативным модулям Python, Node.js и библиотекам, которые компилируются во время RUN.
Кэш работает локально, но не используется в CI/CD
Локальный Docker Engine хранит слои на диске, а временный CI-runner часто удаляет их после job. Кэш может отсутствовать из-за очистки runner, другой ветки, изменившегося базового тега или отличий в build arguments.
Подключите внешний кэш через cache-from и cache-to, проверьте права записи в registry и включите подробный лог:
docker buildx build --progress=plain --cache-from=type=registry,ref=registry.company.local/team/app:buildcache --cache-to=type=registry,ref=registry.company.local/team/app:buildcache,mode=max -t registry.company.local/team/app:commit --push .
В логе ищите importing cache manifest, cache hit или строки CACHED. Разные значения commit, branch и build arguments не всегда ломают весь кэш, но могут изменить отдельные шаги.
Секреты попали в слои образа
Не передавайте токены через ENV, ARG и обычный COPY. Эти значения могут сохраниться в истории, метаданных, кэше или логе сборки.
BuildKit поддерживает временный mount секрета:
RUN --mount=type=secret,id=npm_token sh -c 'TOKEN=$(cat /run/secrets/npm_token) && npm config set //registry.local/:_authToken=$TOKEN && npm ci'
Секрет передают builder отдельно:
docker build --secret id=npm_token,src=./secrets/npm_token -t app:local .
Проверьте, что файл с секретом исключен через .dockerignore, а команда не печатает его содержимое. Для runtime-параметров используйте секретное хранилище оркестратора. Если ключ уже попал в образ, удаление файла недостаточно: отзовите ключ, создайте новый и пересоберите все затронутые теги.
Воспроизводимый и безопасный production-образ
Зафиксировать версии базового образа и зависимостей
Lock-файлы фиксируют версии прикладных пакетов. Тег базового образа задает ожидаемую линию обновлений, а digest фиксирует конкретный набор слоев. Выберите политику заранее: например, собирать release только с digest, регулярно обновлять его отдельным pull request и запускать тесты после обновления.
Полная повторяемость снижает риск, что одна и та же команда даст разный результат через неделю. Регулярные обновления закрывают уязвимости и исправления базового дистрибутива. Эти требования нужно совмещать: старый digest нельзя оставлять навсегда только ради стабильности.
Практические правила фиксации версий, registry, healthcheck и smoke-тестов собраны в материале о воспроизводимых Docker-образах.
Запускать приложение не от root
Создайте отдельного пользователя и назначьте права на рабочий каталог:
FROM python:3.12-slim
WORKDIR /app
RUN groupadd --system app && useradd --system --gid app app
COPY --chown=app:app app.py .
USER app
EXPOSE 8080
CMD ["python", "app.py"]
До публикации проверьте чтение конфигурации и запись только в разрешенные каталоги. Если приложение пишет во временные данные, выделите отдельный каталог и назначьте ему владельца. Не добавляйте права root на весь filesystem ради обхода ошибки доступа: найдите конкретный путь и исправьте его владельца.
Добавить контроль в CI/CD
Минимальный pipeline должен собрать образ, проверить его метаданные, запустить smoke-тест и остановить публикацию при ошибке. Для production добавьте проверку секретов, сканирование уязвимостей, контроль размера и анализ слоев.
Используйте разные теги для разных целей:
app:local, ручная проверка на рабочей станции.app:commit-abc123, результат конкретного commit.app:release-1.4.0, версия, которую можно откатить.app:latest, только если политика проекта четко определяет его смысл.
Публикуйте образ после успешного теста, а не до него. Для временного стенда, CI-runner или Kubernetes-кластера можно арендовать ресурсы в Timeweb Cloud, но доступ к registry, ключи и политика хранения образов должны оставаться под контролем команды.
Готовые практические шаблоны с non-root пользователем, multi-stage сборкой и проверками Dockerfile доступны в руководстве по безопасным Dockerfile.
Итоговый чек-лист сборки Docker-образа
Минимальный алгоритм перед публикацией образа
- Проверьте текущий каталог, путь к Dockerfile и состав build context.
- Создайте
.dockerignoreи исключите Git-метаданные, кэши, логи, секреты и локальные зависимости. - Скопируйте манифесты и lock-файлы раньше исходного кода.
- Соберите образ понятной командой, например
docker build -t app:commit-abc123 .. - Проверьте
docker image inspect,docker imagesиdocker history. - Убедитесь, что в runtime нет компилятора, тестовых зависимостей и исходников, если они не нужны приложению.
- Запустите контейнер без bind mount исходного каталога.
- Проверьте порт, health endpoint, логи и код завершения.
- Проверьте отсутствие секретов в Dockerfile, metadata и слоях.
- Перед публикацией выполните сканирование уязвимостей и зафиксируйте digest базового образа по принятой политике.
Что проверить при переносе сборки в CI/CD
- Зафиксируйте версии зависимостей, базового образа и целевой платформы.
- Проверьте доступ runner к registry, авторизацию и лимиты Docker Hub.
- Настройте
cache-fromиcache-to, если runner не сохраняет локальный кэш. - Сохраните подробный лог BuildKit и убедитесь, что ожидаемые шаги получают cache hit.
- Соберите образ с commit-тегом и запустите smoke-тест в чистом контейнере.
- Проверьте размер, слои, пользователя, healthcheck и команду запуска.
- Опубликуйте release-тег только после успешных проверок.
Рабочий Docker-образ начинается с корректного context и понятного Dockerfile. Правильный порядок инструкций сохраняет кэш, multi-stage build убирает инструменты сборки из runtime, а измерение слоев показывает реальный результат. После локальной проверки повторите те же шаги в CI/CD и публикуйте только образ с контролируемыми версиями, минимальными правами и проверенным запуском.