Когда базовый multi-stage Dockerfile уже есть, следующий шаг — сделать сборку воспроизводимой, безопасной и удобной для CI. На этом уровне обычно всплывают не только размер образа, но и секреты, healthcheck, corporate proxy, линтеры и сканирование.
Если нужен именно старт с компактного Dockerfile, сначала лучше пройти базу: multi-stage сборка, scratch, pinning и runtime-безопасность. А если CI уже упирается в переустановку toolchain и публикацию образов, дальше логично читать про golden builder image и registry.
В статье
- Линтеры Dockerfile и обязательные проверки в CI
- Build-time параметры, токены и proxy
- Аудит слоёв и содержимого образа
- Build-time vs runtime конфигурация
HEALTHCHECKдля сервисов- Минимальный job для GitHub Actions
- Чеклист перед релизом
Линтеры Dockerfile: обязательный шаг в CI
Хороший Dockerfile лучше проверять автоматически, а не только глазами. Базовый вариант:
hadolint DockerfileДля CI удобно запускать линтер через контейнер:
# Stdin-подход: не нужно монтировать файл внутрь контейнера
docker run --rm -i hadolint/hadolint < DockerfileЧто это даёт на практике:
- Ловит небезопасные и хрупкие паттерны в Dockerfile.
- Подсвечивает проблемы воспроизводимости, например непинованные зависимости.
- Даёт единый стандарт качества для всех PR.
Минимальная связка в pipeline: hadolint для best practices и сканер уязвимостей образа, например trivy или grype.
Build-time параметры, токены и прокси
Что можно передавать в сборку
- публичные параметры:
ARG VERSION,ARG COMMIT,ARG TARGETARCH; - proxy-настройки для окружений без прямого доступа в интернет;
- временные токены для скачивания приватных артефактов, но только через секреты BuildKit.
Как передавать токены безопасно
Для секретов используйте BuildKit и --secret, а не ARG:
# Секрет берётся из переменной окружения REPO_TOKEN
# и доступен только во время сборки, не попадает в слои
docker build \
--secret id=repo_token,env=REPO_TOKEN \
-t app:build .# syntax=docker/dockerfile:1.7
# Секрет монтируется как файл в /run/secrets/ — доступен только в этом RUN,
# не сохраняется в слое образа
RUN --mount=type=secret,id=repo_token \
TOKEN="$(cat /run/secrets/repo_token)" && \
curl -H "Authorization: Bearer ${TOKEN}" -fsSL https://repo.example.com/artifact.tgz -o /tmp/artifact.tgzТак токен не попадёт в итоговый образ и историю слоёв.
Практический сценарий: скачать приватный артефакт во время build, распаковать его в builder-стадии и не переносить ничего лишнего в runtime-образ.
Как задавать proxy для сборки
Если build-узел находится за корпоративным proxy, используйте стандартные переменные:
# Стандартные переменные proxy — Docker обрабатывает их особым образом
# и не сохраняет в метаданных образа
docker build \
--build-arg HTTP_PROXY=http://proxy.corp.local:3128 \
--build-arg HTTPS_PROXY=http://proxy.corp.local:3128 \
--build-arg NO_PROXY=localhost,127.0.0.1,.corp.local \
-t app:proxy-build .# Эти ARG нужны только в builder-стадии;
# не объявляйте их в финальном FROM, чтобы proxy не утёк в runtime
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXYПрокси лучше ограничивать build-стадией и не переносить в runtime-образ без необходимости.
Типовой случай: self-hosted runner или внутренний build-агент не имеет прямого выхода в интернет и может скачивать зависимости только через корпоративный proxy или внутренний cache.
Чем проверить содержимое слоёв
Полезные инструменты для аудита образа:
diveпоказывает, какие файлы попали в каждый слой и где появляется лишний вес.docker historyдаёт быстрый обзор команд, сформировавших слои.syftгенерирует SBOM и показывает, какие пакеты и зависимости внутри образа.trivy imageпроверяет уязвимости и одновременно помогает понять состав образа.
Минимальный набор команд:
docker history app:multi # быстрый обзор команд и размера слоёв
dive app:multi # интерактивный анализ содержимого каждого слоя
syft app:multi # SBOM: список пакетов и зависимостей в образе
trivy image app:multi # сканирование на известные уязвимостиГигиена переменных окружения: build-time vs runtime
Частая ошибка — смешивать переменные этапа сборки и этапа выполнения.
Что относится к build-time
ARG, напримерVERSIONиCOMMIT, нужны для сборки и метаданных;- эти значения должны быть не секретными, потому что их легко “засветить” в истории слоёв.
Что относится к runtime
ENVи переменные, которые передаются приdocker runили вdocker compose;- всё, что связано с инфраструктурой:
DATABASE_URL, токены, ключи API, endpoints; - эти значения должны задаваться снаружи образа, а не в Dockerfile.
Практическое правило:
- Если переменная нужна только чтобы собрать бинарник, используйте
ARG. - Если переменная нужна приложению во время работы контейнера, передавайте её как runtime env.
- Секреты не храните в Git, Dockerfile и шаблонах с реальными значениями.
Наглядно:
FROM golang:1.26-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build-time аргументы: версия и коммит вшиваются в бинарник через ldflags
ARG VERSION=dev
ARG COMMIT=unknown
RUN CGO_ENABLED=0 go build -ldflags="-X main.Version=${VERSION} -X main.Commit=${COMMIT}" -o server ./cmd/server
FROM alpine:3.23
COPY --from=builder /app/server /usr/local/bin/server
CMD ["server"]VERSION и COMMIT логично держать на сборке, а DATABASE_URL и ключи — только в runtime-конфигурации.
HEALTHCHECK в Docker: обязательно для сервисов
HEALTHCHECK помогает понять, что контейнер не просто запущен, а действительно готов обслуживать запросы.
Важно: Docker оценивает здоровье контейнера по коду завершения команды. HTTP здесь лишь один из возможных способов проверки.
Для HTTP-сервиса обычно достаточно endpoint вида /api/health:
FROM alpine:3.23
RUN apk --no-cache add ca-certificates wget && addgroup -S app && adduser -S app -G app
COPY --from=builder /app/server /usr/local/bin/server
USER app
# start-period — время на старт приложения, в течение которого
# неудачные проверки не считаются провалом
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://127.0.0.1:8081/api/health > /dev/null || exit 1
CMD ["server"]Практика:
- Проверяйте, что сервис действительно отвечает и способен обслуживать запросы, а не только что процесс жив.
- Не делайте слишком частый healthcheck, чтобы не создавать лишнюю нагрузку.
- Держите таймауты и
retriesреалистичными под ваш старт и сеть.
Пример для PostgreSQL-контейнера:
FROM postgres:16-alpine
# pg_isready — штатная утилита PostgreSQL для проверки готовности
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=5 \
CMD pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" -h 127.0.0.1 -p 5432 || exit 1Минимальный job для GitHub Actions
name: docker-quality
on:
pull_request:
paths:
- 'Dockerfile'
- '**/Dockerfile'
push:
branches: [main, develop]
jobs:
lint-and-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Hadolint
run: docker run --rm -i hadolint/hadolint < Dockerfile
- name: Build image
run: docker build -t app:ci .
- name: Trivy scan (fail on HIGH/CRITICAL)
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: app:ci
severity: HIGH,CRITICAL
exit-code: '1' # pipeline упадёт при наличии HIGH/CRITICAL уязвимостейЧеклист перед релизом
- Секреты не попадают в Dockerfile, слои и логи сборки.
- Dockerfile проходит линтер в CI.
- Есть сканирование уязвимостей.
- Переменные сборки и runtime-конфигурация не смешаны.
- Для сервиса настроен
HEALTHCHECK.
Production-сборка — это не одна “магическая” команда docker build, а набор дисциплин вокруг Dockerfile. Чем раньше они появляются в проекте, тем меньше боли в CI и эксплуатации.
Основа всего этого — аккуратный multi-stage Dockerfile. Если же вам уже тесно в рамках одного builder-окружения, следующий логичный шаг — golden builder image и продуманная схема публикации в registry.
Комментарии