Главная/Dockerfile/Практика

Раздел 5. Практические задания

Задания выполняются в реальной системе. Разбор открывайте только после самостоятельной попытки.

Обозначения: [обяз.] — обязательное, [доп.] — дополнительное, [★] — повышенной сложности, [диаг.] — диагностическое.

Подготовка:

bash
mkdir -p ~/docker-course/05-dockerfile
cd ~/docker-course/05-dockerfile
# docker pull принимает РОВНО один образ:
# `docker pull a b` отвечает «docker pull requires 1 argument»
for img in python:3.13-slim debian:trixie-slim alpine:3.21; do docker pull -q "$img"; done
docker system df    # зафиксируйте исходное состояние

Задание 1. Измерение build context [обяз.]

Постановка. Создайте проект с тяжёлыми каталогами (.git, .venv, __pycache__) и файлом .env. Измерьте размер контекста до и после добавления .dockerignore.

Требуется показать три числа: размер каталога, размер контекста без исключений, размер контекста с исключениями. Дополнительно докажите, что .env больше не попадает в образ.

Ожидаемый результат. Сокращение контекста минимум в сто раз и отсутствие .env в образе.

Проверка:

bash
docker build -q -t ctx:check -f- . <<'EOF'
FROM alpine:3.21
WORKDIR /c
COPY . .
CMD ["du","-sh","/c"]
EOF
docker run --rm ctx:check

Полное решение — в уроке 5.1.


Задание 2. ARG против ENV [обяз.]

Постановка. Соберите образ, в котором:

  1. ARG доступен при сборке и недоступен в container.
  2. ENV доступен и там, и там.
  3. Значение, переданное через --build-arg, попадает в переменную окружения работающего container.

Затем покажите, что секрет, переданный через --build-arg, извлекается из готового образа, и исправьте это через secret mount.

Ожидаемый результат. Демонстрация всех трёх пунктов и доказательство отсутствия секрета после исправления.

Проверка:

bash
docker history --no-trunc <образ> | grep -o 'SECRET=[^ ]*' || echo "секрет не найден"
Разбор
bash
mkdir -p /tmp/ex5-2 && cd /tmp/ex5-2

cat > Dockerfile.demo <<'EOF'
FROM alpine:3.21
ARG BUILD_ONLY="только сборка"
ARG APP_ENV=production
ENV RUNTIME_VAR="и сборка, и запуск"
ENV APP_ENV=${APP_ENV}
RUN echo "при сборке ARG: ${BUILD_ONLY}"
CMD ["sh", "-c", "echo \"ARG: [${BUILD_ONLY}]\"; echo \"ENV: [${RUNTIME_VAR}]\"; echo \"APP_ENV: [${APP_ENV}]\""]
EOF

docker build -q -f Dockerfile.demo --build-arg APP_ENV=development -t ex52:demo . > /dev/null
docker run --rm ex52:demo
text
ARG: []
ENV: [и сборка, и запуск]
APP_ENV: [development]

Пункты 1–3 выполнены. Обратите внимание на порядок в Dockerfile: ARG APP_ENV объявлен до ENV APP_ENV=${APP_ENV}. Обратный порядок не сработал бы: при ENV APP_ENV=… перед ARG APP_ENV значение из --build-arg до ENV не доходит. Побеждает объявленная последней инструкция — измерено в уроке 5.2.

Утечка через --build-arg:

bash
cat > Dockerfile.leak <<'EOF'
FROM alpine:3.21
ARG SECRET
RUN echo "длина секрета: ${#SECRET}" > /log.txt
CMD ["cat", "/log.txt"]
EOF

docker build -q -f Dockerfile.leak --build-arg SECRET='P@ssw0rd-Very-Secret' -t ex52:leak . > /dev/null
docker run --rm ex52:leak
echo "--- извлечение из истории ---"
docker history --no-trunc ex52:leak | grep -o 'SECRET=[^ ]*' | head -1
text
длина секрета: 20
--- извлечение из истории ---
SECRET=P@ssw0rd-Very-Secret

Исправление:

bash
echo -n 'P@ssw0rd-Very-Secret' > secret.txt
cat > Dockerfile.safe <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.21
RUN --mount=type=secret,id=pw,required=true \
    echo "длина секрета: $(wc -c < /run/secrets/pw)" > /log.txt
CMD ["cat", "/log.txt"]
EOF

docker build -q -f Dockerfile.safe --secret id=pw,src=secret.txt -t ex52:safe . > /dev/null
docker run --rm ex52:safe
echo "--- поиск в истории ---"
docker history --no-trunc ex52:safe | grep -c 'P@ssw0rd' || echo "не найден"
echo "--- поиск в слоях ---"
docker save ex52:safe -o /tmp/s.tar && grep -qa 'P@ssw0rd' /tmp/s.tar \
    && echo "НАЙДЕН в слоях" || echo "не найден в слоях"
rm -f /tmp/s.tar secret.txt

cd /tmp && docker rmi -f ex52:demo ex52:leak ex52:safe > /dev/null 2>&1; rm -rf /tmp/ex5-2
text
длина секрета: 20
--- поиск в истории ---
не найден
--- поиск в слоях ---
не найден в слоях

Результат сборки идентичен, но секрета в образе нет ни в метаданных, ни в файловой системе. Параметр required=true гарантирует, что при отсутствии секрета сборка упадёт явно, а не соберётся с пустым значением.


Задание 3. Поломка и починка кэша [обяз.]

Постановка. Соберите образ, намеренно сломайте кэш и по выводу сборки определите инструкцию-виновника.

  1. Соберите Python-приложение с COPY . . перед pip install.
  2. Измерьте время пересборки после изменения одной строки кода.
  3. Определите по выводу, какая инструкция инвалидировалась первой.
  4. Исправьте порядок и измерьте снова.

Ожидаемый результат. Два замера с разницей не менее чем в 10 раз и указание точки каскада.

Проверка:

bash
docker build -f <dockerfile> -t app . 2>&1 | grep -E '=> \[[0-9]+/[0-9]+\]'

Первая строка без CACHED — точка каскада.

Полное решение — в уроке 5.5.


Задание 4. Таблица CMD и ENTRYPOINT [обяз.]

Постановка. Экспериментально постройте таблицу результирующих команд для восьми комбинаций ENTRYPOINT и CMD (есть/нет × exec/shell form).

Для каждой определите: что выполняется без аргументов, что с аргументом, кто является PID 1.

Отдельно объясните две строки, где CMD игнорируется полностью.

Ожидаемый результат. Таблица из восьми строк и объяснение механизма игнорирования.

Полное решение — в уроке 5.4.


Задание 5. Multi-stage build [обяз.]

Постановка. Преобразуйте однослойную сборку приложения с компилируемой зависимостью (psycopg2) в multi-stage.

Требования:

  1. Финальный образ минимум вдвое меньше.
  2. В финальном образе нет gcc и make.
  3. Приложение работает идентично.
  4. Есть общая базовая стадия без дублирования.

Ожидаемый результат. Два образа с замерами размера и подтверждение отсутствия компиляторов.

Проверка:

bash
docker run --rm <образ> sh -c 'command -v gcc >/dev/null && echo есть || echo нет'

Полное решение — в уроке 5.7.


Задание 6. Cache mount [доп.]

Постановка. Добавьте cache mount для pip и измерьте эффект при изменении списка зависимостей.

Дополнительно докажите, что кэш не попадает в образ — сравните размеры образов с cache mount и без него.

Ожидаемый результат. Ускорение не менее чем в три раза при одинаковом размере образа.

Разбор

Полная реализация — в уроке 5.6. Ключевые моменты:

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
COPY app/ ./app/
CMD ["python", "app/main.py"]

Две детали, которые легко упустить:

Директива # syntax обязательна. Без неё BuildKit не распознает --mount и сборка упадёт с синтаксической ошибкой.

Флаг --no-cache-dir нужно убрать. Он и cache mount взаимоисключают друг друга: первый запрещает pip создавать кэш, второй его сохраняет. Оставив оба, вы получите замедление вместо ускорения — cache mount будет монтироваться, но оставаться пустым.

Проверка, что кэш не в образе:

bash
docker run --rm <образ> ls /root/.cache/pip 2>&1 | tail -1
text
ls: cannot access '/root/.cache/pip': No such file or directory

Размеры образов с cache mount и без совпадают — данные живут в состоянии BuildKit, а не в слоях.


Задание 7. HEALTHCHECK с медленным стартом [доп.]

Постановка. Настройте healthcheck для приложения, стартующего 25 секунд, так чтобы:

  1. Container не помечался unhealthy во время инициализации.
  2. Готовность обнаруживалась в течение 3 секунд после её наступления.
  3. Отказ обнаруживался не дольше чем за 30 секунд.

Обоснуйте каждое выбранное значение параметра.

Ожидаемый результат. Подобранные параметры с обоснованием и лог переходов статуса.

Проверка:

bash
docker inspect <container> --format '{{.State.Health.Status}} {{.State.Health.FailingStreak}}'

Полное решение — в уроке 5.8.


Задание 8. Воспроизводимость [доп.]

Постановка. Приведите сборку к составной воспроизводимости: закрепите базовый образ по digest и все Python-зависимости, включая транзитивные.

Докажите, что две независимые сборки дают идентичный набор пакетов. Напишите скрипт проверки актуальности закреплённого digest.

Ожидаемый результат. Dockerfile с digest, lock file и работающий скрипт проверки с ненулевым exit code при устаревании.

Полное решение — в уроке 5.9.


Задание 9. Диагностика: медленная сборка [диаг.]

Постановка. Дан Dockerfile, где каждая сборка занимает около четырёх минут, хотя зависимости не менялись. Найдите три независимые причины и устраните каждую, измерив эффект по отдельности.

dockerfile
FROM python:3.13-slim

RUN apt-get update
RUN apt-get install -y build-essential curl git
RUN rm -rf /var/lib/apt/lists/*

COPY . /app
WORKDIR /app

RUN pip install --no-cache-dir -r requirements.txt
RUN chmod +x /app/entrypoint.sh
RUN chown -R nobody /app

USER nobody
CMD ["/app/entrypoint.sh"]

Проект содержит каталоги .git (25 MB), .venv (60 MB) и __pycache__. Файла .dockerignore нет.

Подсказка 1

Первая причина связана с тем, что передаётся daemon перед началом сборки.

Подсказка 2

Вторая — порядок инструкций: что инвалидируется при изменении кода.

Подсказка 3

Третья — инструкции, которые дублируют данные из-за copy-up.

Разбор
bash
#!/usr/bin/env bash
# diagnose-slow-build.sh — три причины медленной сборки.
set -uo pipefail

WORK="$(mktemp -d)"
trap 'docker rmi -f $(docker images -q --filter "reference=slow:*") >/dev/null 2>&1 || true;
      rm -rf "$WORK"' EXIT
cd "$WORK"

mkdir -p app .git .venv __pycache__
cat > requirements.txt <<'EOF'
fastapi==0.141.1
uvicorn[standard]==0.52.0
pydantic==2.13.4
EOF
echo "print('версия 1')" > app/main.py
printf '#!/bin/sh\nexec python /app/app/main.py\n' > entrypoint.sh
dd if=/dev/urandom of=.git/pack.bin bs=1M count=25 status=none
dd if=/dev/urandom of=.venv/lib.so bs=1M count=60 status=none
dd if=/dev/urandom of=__pycache__/m.pyc bs=1K count=100 status=none

# ── v0: исходный ──
cat > Dockerfile.v0 <<'EOF'
FROM python:3.13-slim
RUN apt-get update
RUN apt-get install -y build-essential curl git
RUN rm -rf /var/lib/apt/lists/*
COPY . /app
WORKDIR /app
RUN pip install --no-cache-dir -r requirements.txt
RUN chmod +x /app/entrypoint.sh
RUN chown -R nobody /app
USER nobody
CMD ["/app/entrypoint.sh"]
EOF

# ── v1: + .dockerignore (причина 1) ──
cp Dockerfile.v0 Dockerfile.v1

# ── v2: + порядок COPY (причина 2) ──
cat > Dockerfile.v2 <<'EOF'
FROM python:3.13-slim
RUN apt-get update \
 && apt-get install -y --no-install-recommends build-essential curl git \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY entrypoint.sh app/ ./
RUN chmod +x /app/entrypoint.sh
RUN chown -R nobody /app
USER nobody
CMD ["/app/entrypoint.sh"]
EOF

# ── v3: + COPY --chown --chmod вместо RUN (причина 3) ──
cat > Dockerfile.v3 <<'EOF'
FROM python:3.13-slim
RUN apt-get update \
 && apt-get install -y --no-install-recommends build-essential curl git \
 && rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --uid 10001 appuser
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=appuser:appuser --chmod=755 entrypoint.sh ./entrypoint.sh
COPY --chown=appuser:appuser app/ ./app/
USER 10001:10001
CMD ["/app/entrypoint.sh"]
EOF

measure() {
    local df="$1" tag="$2" label="$3" n="$4"
    docker build -q -f "$df" -t "$tag" . > /dev/null 2>&1
    echo "print('версия $n')" > app/main.py
    local s e
    s="$(date +%s.%N)"
    docker build -q -f "$df" -t "$tag" . > /dev/null 2>&1
    e="$(date +%s.%N)"
    printf '  %-38s %7.2f c   %s\n' "$label" \
        "$(awk -v a="$s" -v b="$e" 'BEGIN{print b-a}')" \
        "$(docker images "$tag" --format '{{.Size}}')"
}

echo "═══ Пересборка после изменения одной строки кода ═══"
measure Dockerfile.v0 slow:v0 "v0: исходный" 2

cat > .dockerignore <<'EOF'
.git
.venv
__pycache__
*.pyc
Dockerfile*
.dockerignore
EOF
measure Dockerfile.v1 slow:v1 "v1: + .dockerignore" 3
measure Dockerfile.v2 slow:v2 "v2: + порядок COPY" 4
measure Dockerfile.v3 slow:v3 "v3: + COPY --chown/--chmod" 5

echo
echo "═══ Вклад RUN chown -R в размер ═══"
docker history slow:v2 --format '{{.Size}}\t{{.CreatedBy}}' | grep -i chown | head -1

Ожидаемый вывод:

text
═══ Пересборка после изменения одной строки кода ═══
  v0: исходный                            241.83 c   478MB
  v1: + .dockerignore                      38.12 c   397MB
  v2: + порядок COPY                        3.47 c   391MB
  v3: + COPY --chown/--chmod                1.08 c   339MB

═══ Вклад RUN chown -R в размер ═══
52.1MB	RUN /bin/sh -c chown -R nobody /app # buildkit

Три причины и их вклад.

Причина 1 — отсутствие .dockerignore. Каталоги .git и .venv (85 MB) передавались daemon при каждой сборке и попадали в образ через COPY . /app. Хуже того, любое изменение в них инвалидировало COPY, а за ним каскадом — установку зависимостей. Устранение: 242 → 38 секунд.

Причина 2 — порядок инструкций. COPY . /app стоял перед pip install, поэтому изменение любого файла проекта пересобирало зависимости (урок 5.5). Устранение: 38 → 3.5 секунды. Заодно объединены три инструкции apt-get, что защищает от cache busting (урок 5.3).

Причина 3 — RUN chown -R и RUN chmod. Изменение метаданных файла вызывает copy-up, поэтому chown -R /app продублировал весь каталог приложения в новом слое — 52 MB. Флаги --chown и --chmod у COPY задают владельца и права в момент записи, без дополнительного слоя. Устранение: 3.5 → 1.1 секунды и минус 52 MB.

Итог: 242 → 1.1 секунды, в 220 раз быстрее. Образ уменьшился с 478 до 339 MB.

Что осталось неоптимальным. В образе присутствуют build-essential и git — около 200 MB, нужных только при сборке. Это решается multi-stage build (урок 5.7) и доводит образ примерно до 200 MB.

Обратите внимание на замену USER nobody на выделенного пользователя с фиксированным UID. Пользователь nobody разделяется со всей системой и не имеет домашнего каталога — это отдельная проблема, разбираемая в разделе 06.


Задание 10. Диагностика: аргументы не доходят [диаг.]

Постановка. Дан образ, который игнорирует всё, что ему передают: ни CMD, ни аргументы docker run не влияют на поведение.

dockerfile
FROM python:3.13-slim
COPY app.py /app.py
ENTRYPOINT python /app.py
CMD ["--default-mode"]

Требуется:

  1. Воспроизвести проблему.
  2. Объяснить механизм.
  3. Предложить два варианта исправления.
  4. Написать проверку, обнаруживающую эту проблему в чужом образе.
Разбор
bash
mkdir -p /tmp/ex5-10 && cd /tmp/ex5-10

cat > app.py <<'PY'
import sys
print(f"полученные аргументы: {sys.argv[1:]}")
PY

cat > Dockerfile.broken <<'EOF'
FROM python:3.13-slim
COPY app.py /app.py
ENTRYPOINT python /app.py
CMD ["--default-mode"]
EOF

docker build -q -f Dockerfile.broken -t ex510:broken . > /dev/null

echo "=== 1. Воспроизведение ==="
echo -n "  без аргументов: "; docker run --rm ex510:broken
echo -n "  с аргументом:   "; docker run --rm ex510:broken --custom-mode
text
=== 1. Воспроизведение ===
  без аргументов: полученные аргументы: []
  с аргументом:   полученные аргументы: []

Оба раза пусто: CMD проигнорирован, аргумент проигнорирован.

2. Механизм.

bash
docker image inspect ex510:broken --format 'Entrypoint: {{json .Config.Entrypoint}}'
docker image inspect ex510:broken --format 'Cmd:        {{json .Config.Cmd}}'
text
Entrypoint: ["/bin/sh","-c","python /app.py"]
Cmd:        ["--default-mode"]

ENTRYPOINT записан в shell form, поэтому превратился в /bin/sh -c "python /app.py".

Docker формирует итоговую команду конкатенацией Entrypoint + Cmd:

text
/bin/sh -c "python /app.py" "--default-mode"

Оболочка с флагом -c берёт первый аргумент как команду, а остальные становятся позиционными параметрами: $0 = --default-mode. Команда python /app.py их не использует — они теряются.

С аргументами docker run происходит то же самое: они попадают в ту же позицию.

3. Два варианта исправления.

Вариант A — exec form:

bash
cat > Dockerfile.fix-a <<'EOF'
FROM python:3.13-slim
COPY app.py /app.py
ENTRYPOINT ["python", "/app.py"]
CMD ["--default-mode"]
EOF

docker build -q -f Dockerfile.fix-a -t ex510:fix-a . > /dev/null
echo -n "  A без аргументов: "; docker run --rm ex510:fix-a
echo -n "  A с аргументом:   "; docker run --rm ex510:fix-a --custom-mode
text
  A без аргументов: полученные аргументы: ['--default-mode']
  A с аргументом:   полученные аргументы: ['--custom-mode']

Вариант B — shell form с exec "$@", если возможности оболочки всё же нужны:

bash
cat > entrypoint.sh <<'SH'
#!/bin/sh
set -e
# здесь может быть подготовка с использованием возможностей оболочки
exec python /app.py "$@"
SH

cat > Dockerfile.fix-b <<'EOF'
FROM python:3.13-slim
COPY app.py /app.py
COPY --chmod=755 entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["--default-mode"]
EOF

docker build -q -f Dockerfile.fix-b -t ex510:fix-b . > /dev/null
echo -n "  B без аргументов: "; docker run --rm ex510:fix-b
echo -n "  B с аргументом:   "; docker run --rm ex510:fix-b --custom-mode
text
  B без аргументов: полученные аргументы: ['--default-mode']
  B с аргументом:   полученные аргументы: ['--custom-mode']

Оба варианта работают. Вариант A проще и предпочтителен, когда подготовка не нужна. Вариант B нужен, если перед запуском требуется что-то сделать — но тогда обязательны exec и "$@" в кавычках.

4. Проверка чужого образа.

bash
cat > check-entrypoint.sh <<'CHECK'
#!/usr/bin/env bash
# check-entrypoint.sh — обнаружение ENTRYPOINT в shell form.
set -uo pipefail

IMG="${1:?Использование: $0 <образ>}"
docker image inspect "$IMG" > /dev/null 2>&1 || {
    echo "образ не найден: $IMG" >&2; exit 2; }

ep="$(docker image inspect "$IMG" --format '{{json .Config.Entrypoint}}')"
cmd="$(docker image inspect "$IMG" --format '{{json .Config.Cmd}}')"

printf 'образ:      %s\n' "$IMG"
printf 'Entrypoint: %s\n' "$ep"
printf 'Cmd:        %s\n' "$cmd"
echo

if [[ "$ep" == *'"/bin/sh","-c"'* ]] || [[ "$ep" == *'"/bin/bash","-c"'* ]]; then
    echo "[!] ENTRYPOINT записан в shell form."
    echo "    Последствия:"
    echo "      - CMD игнорируется полностью"
    echo "      - аргументы docker run игнорируются"
    echo "      - PID 1 это /bin/sh, SIGTERM не дойдёт до приложения"
    echo "    Исправление: exec form либо exec \"\$@\" в entrypoint-скрипте."
    exit 1
fi

if [ "$ep" = "null" ] && [[ "$cmd" == *'"/bin/sh","-c"'* ]]; then
    echo "[~] CMD записан в shell form."
    echo "    Аргументы docker run будут работать (они заменяют CMD),"
    echo "    но PID 1 это /bin/sh — SIGTERM не дойдёт до приложения."
    exit 1
fi

echo "[+] Проблем с формой записи не обнаружено."
CHECK
chmod +x check-entrypoint.sh

for t in broken fix-a fix-b; do
    ./check-entrypoint.sh "ex510:$t" | tail -3
    echo "---"
done

cd /tmp && docker rmi -f ex510:broken ex510:fix-a ex510:fix-b > /dev/null 2>&1
rm -rf /tmp/ex5-10
text
[!] ENTRYPOINT записан в shell form.
    Исправление: exec form либо exec "$@" в entrypoint-скрипте.
---
[+] Проблем с формой записи не обнаружено.
---
[+] Проблем с формой записи не обнаружено.
---

Скрипт возвращает ненулевой exit code при обнаружении проблемы, что делает его пригодным для CI: проверку можно включить в pipeline и не допускать публикации образов с этим дефектом.

Почему это важно проверять автоматически. Проблема молчаливая: сборка проходит, container запускается, приложение работает. Обнаруживается она обычно в момент, когда кто-то пытается передать параметр — часто уже в production.


Задание 11. Полная оптимизация [★]

Постановка. Дан Dockerfile реального Python-сервиса. Примените все приёмы раздела и доведите результат до целевых показателей.

dockerfile
FROM python:latest

MAINTAINER team@example.com

ADD https://github.com/example/tool/releases/download/v2.0/tool.tar.gz /tmp/
RUN cd /tmp && tar -xzf tool.tar.gz && mv tool /usr/local/bin/

RUN apt-get update
RUN apt-get install -y build-essential libpq-dev curl git vim
RUN rm -rf /var/lib/apt/lists/*

ARG DB_PASSWORD
ENV DB_PASSWORD=${DB_PASSWORD}

COPY . /app
WORKDIR /app

RUN pip install -r requirements.txt
RUN pip install pytest ruff
RUN ruff check app/
RUN pytest tests/
RUN chmod +x /app/entrypoint.sh
RUN chown -R nobody /app

USER nobody
EXPOSE 8000
ENTRYPOINT /app/entrypoint.sh

Целевые показатели:

ПоказательЦель
Размер образаменее 250 MB
Пересборка после изменения кодаменее 3 секунд
Секретов в образеноль
Компиляторов в финальном образеноль
Аргументы доходят до приложенияда
docker stopменее 2 секунд
Проверки останавливают сборкуда
Работает от non-root с фиксированным UIDда

Для каждого исправления укажите: что было, что стало, какой показатель улучшен.

Подсказки

Подсказка 1

В Dockerfile не менее двенадцати отдельных проблем. Идите по урокам раздела: контекст, базовые инструкции, COPY/RUN, CMD/ENTRYPOINT, кэш, BuildKit, multi-stage, healthcheck, воспроизводимость.

Подсказка 2

Проверки (ruff, pytest) должны быть в отдельной стадии, не входящей в финальный образ, но запускаемой через --target.

Подсказка 3

ENTRYPOINT в shell form — причина сразу двух провалов: аргументы не доходят и docker stop занимает 10 секунд.

Решение

Сначала выполните задание самостоятельно.

Показать решение

Найденные проблемы.

ПроблемаПоказательУрок
1FROM python:latestвоспроизводимость5.9
2MAINTAINER устарел5.2
3ADD по URL без --checksumвоспроизводимость5.3
4Три RUN с apt-get раздельноразмер, cache busting5.3
5vim, git в образеразмер5.3
6Секрет через ARGENVсекреты5.2, 5.6
7COPY . /app до pip installскорость пересборки5.5
8pytest, ruff в финальном образеразмер5.7
9Компиляторы в финальном образеразмер5.7
10RUN chmod и RUN chown -Rразмер5.3
11USER nobodyбезопасность5.2
12ENTRYPOINT в shell formаргументы, docker stop5.4
13Нет HEALTHCHECKнаблюдаемость5.8
14Нет .dockerignoreконтекст, скорость5.1

Итоговый Dockerfile:

dockerfile
# syntax=docker/dockerfile:1

# ── Общая база: настройки и пользователь ──
FROM python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251 AS base

LABEL org.opencontainers.image.authors="team@example.com" \
      org.opencontainers.image.source="https://github.com/example/service"

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PATH="/opt/venv/bin:$PATH"

WORKDIR /app
RUN useradd --create-home --uid 10001 appuser

# ── Сборка: компиляторы остаются здесь ──
FROM base AS builder

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
    rm -f /etc/apt/apt.conf.d/docker-clean \
 && apt-get update \
 && apt-get install -y --no-install-recommends build-essential libpq-dev

RUN python -m venv /opt/venv

COPY requirements.lock .
RUN --mount=type=cache,target=/root/.cache/pip \
    --mount=type=secret,id=db_password,required=true \
    PIP_INDEX_URL="https://$(cat /run/secrets/db_password)@pypi.internal/simple" \
    pip install -r requirements.lock

# ── Проверки: не входят в финальный образ ──
FROM builder AS check
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install pytest==9.1.1 ruff==0.16.0
COPY app/ ./app/
COPY tests/ ./tests/
RUN ruff check app/
RUN pytest -q tests/

# ── Финальный образ ──
FROM base AS runtime

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
    rm -f /etc/apt/apt.conf.d/docker-clean \
 && apt-get update \
 && apt-get install -y --no-install-recommends libpq5

ADD --checksum=sha256:0000000000000000000000000000000000000000000000000000000000000000 \
    https://github.com/example/tool/releases/download/v2.0/tool.tar.gz /tmp/tool.tar.gz
RUN tar -xzf /tmp/tool.tar.gz -C /usr/local/bin && rm /tmp/tool.tar.gz

COPY --from=builder --chown=appuser:appuser /opt/venv /opt/venv
COPY --chown=appuser:appuser --chmod=755 entrypoint.sh ./entrypoint.sh
COPY --chown=appuser:appuser app/ ./app/

USER 10001:10001
EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --start-interval=2s --retries=3 \
  CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2).status==200 else 1)"

ENTRYPOINT ["./entrypoint.sh"]
CMD ["--port", "8000"]

И entrypoint.sh:

bash
#!/bin/sh
set -e
# подготовка при необходимости
exec python -m app.main "$@"

И .dockerignore:

text
.git
.venv
__pycache__
*.py[cod]
.pytest_cache
.ruff_cache
.env
.env.*
*.pem
*.key
Dockerfile*
.dockerignore
docs

tests в списке нет намеренно: стадия test копирует этот каталог, а .dockerignore действует на все стадии сразу (урок 5.1). В финальный образ тесты не попадают потому, что стадия runtime их не копирует, а не потому, что они исключены из контекста.

Разбор ключевых решений.

Проблема 6 — секрет. Был ARG DB_PASSWORD с последующим ENV, то есть секрет попадал и в историю, и в конфигурацию образа. Стал secret mount с required=true: доступен только внутри одной инструкции RUN, в образе не остаётся.

Проблема 8 — стадия проверок. Стадия check наследуется от builder, поэтому переиспользует установленные зависимости, но не входит в цепочку финального образа. При обычной сборке BuildKit её пропускает. В CI запускается явно:

bash
docker build --target check -t app:check .   # проверки, падение останавливает pipeline
docker build -t app:latest .                  # публикуемый образ

Проблема 12 — ENTRYPOINT в shell form. Исправление даёт сразу два улучшения: аргументы теперь доходят до приложения, и приложение становится PID 1, поэтому docker stop завершает его за доли секунды вместо десяти.

Проблема 11 — USER nobody. Пользователь nobody разделяется со всеми процессами системы и не имеет домашнего каталога. Выделенный пользователь с фиксированным UID 10001 предпочтителен: он изолирован, а числовой UID корректно работает при монтировании volumes и в Kubernetes с runAsNonRoot.

Достижение целевых показателей:

ПоказательБылоСтало
Размер образа~980 MB~215 MB
Пересборка после изменения кода~180 c~1.2 c
Секретов в образе10
Компиляторовестьнет
Аргументы доходятнетда
docker stop10.3 c0.4 c
Проверки блокируют сборкунет (в образе)да (стадия check)
Non-root с фиксированным UIDnobody10001

Проверочный набор команд:

bash
# размер
docker images app:latest --format '{{.Size}}'

# секреты
docker history --no-trunc app:latest | grep -ci 'password' || echo "0"

# компиляторы
docker run --rm --entrypoint sh app:latest -c 'command -v gcc || echo "нет"'

# аргументы
docker run --rm app:latest --port 9000

# пользователь
docker run --rm --entrypoint id app:latest

# graceful shutdown
docker run -d --name t app:latest; sleep 2
time docker stop t; docker inspect t --format '{{.State.ExitCode}}'; docker rm t

Очистка после раздела

bash
# ВНИМАНИЕ: НЕ `docker ps -aq | xargs -r docker rm -f`.
# Такая строка удаляет ВСЕ container'ы на машине, включая чужие:
# базу коллеги, кластер kind, работающий стенд. Удаляем только
# созданные из образов этого раздела.
for img in python:3.13-slim debian:trixie-slim alpine:3.21; do
    docker ps -aq --filter "ancestor=$img" | xargs -r docker rm -f
done
docker image prune -f
docker builder prune -f
docker system df

Сравните с состоянием, зафиксированным в начале раздела.


Критерии завершения

Раздел закрыт, когда выполнены обязательные задания 1–5 и вы можете без подсказок ответить на вопросы из MAIN.md раздела.

Дальше: Quiz 05.


Навигация

← Предыдущий материал
Вернуться к разделу
Следующий раздел → Python внутри Container
Главное оглавление

Markdown на GitHub ↗