Production-сборка Docker-образов: secrets, proxy, healthcheck и CI

Практики production-сборки Docker-образов: линтеры, сканирование, BuildKit secrets, proxy, healthcheck и разделение build-time и runtime

Когда базовый multi-stage Dockerfile уже есть, следующий шаг — сделать сборку воспроизводимой, безопасной и удобной для CI. На этом уровне обычно всплывают не только размер образа, но и секреты, healthcheck, corporate proxy, линтеры и сканирование.

Если нужен именно старт с компактного Dockerfile, сначала лучше пройти базу: multi-stage сборка, scratch, pinning и runtime-безопасность. А если CI уже упирается в переустановку toolchain и публикацию образов, дальше логично читать про golden builder image и registry.

В статье

Линтеры Dockerfile: обязательный шаг в CI

Хороший Dockerfile лучше проверять автоматически, а не только глазами. Базовый вариант:

hadolint Dockerfile

Для CI удобно запускать линтер через контейнер:

# Stdin-подход: не нужно монтировать файл внутрь контейнера
docker run --rm -i hadolint/hadolint < Dockerfile

Что это даёт на практике:

  1. Ловит небезопасные и хрупкие паттерны в Dockerfile.
  2. Подсвечивает проблемы воспроизводимости, например непинованные зависимости.
  3. Даёт единый стандарт качества для всех 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.

Чем проверить содержимое слоёв

Полезные инструменты для аудита образа:

  1. dive показывает, какие файлы попали в каждый слой и где появляется лишний вес.
  2. docker history даёт быстрый обзор команд, сформировавших слои.
  3. syft генерирует SBOM и показывает, какие пакеты и зависимости внутри образа.
  4. 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.

Практическое правило:

  1. Если переменная нужна только чтобы собрать бинарник, используйте ARG.
  2. Если переменная нужна приложению во время работы контейнера, передавайте её как runtime env.
  3. Секреты не храните в 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"]

Практика:

  1. Проверяйте, что сервис действительно отвечает и способен обслуживать запросы, а не только что процесс жив.
  2. Не делайте слишком частый healthcheck, чтобы не создавать лишнюю нагрузку.
  3. Держите таймауты и 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 уязвимостей

Чеклист перед релизом

  1. Секреты не попадают в Dockerfile, слои и логи сборки.
  2. Dockerfile проходит линтер в CI.
  3. Есть сканирование уязвимостей.
  4. Переменные сборки и runtime-конфигурация не смешаны.
  5. Для сервиса настроен HEALTHCHECK.

Production-сборка — это не одна “магическая” команда docker build, а набор дисциплин вокруг Dockerfile. Чем раньше они появляются в проекте, тем меньше боли в CI и эксплуатации.

Основа всего этого — аккуратный multi-stage Dockerfile. Если же вам уже тесно в рамках одного builder-окружения, следующий логичный шаг — golden builder image и продуманная схема публикации в registry.

Обсуждение в Telegram

Присоединиться →

Комментарии