Раздел 5. Практические задания
Задания выполняются в реальной системе. Разбор открывайте только после самостоятельной попытки.
Обозначения: [обяз.] — обязательное, [доп.] — дополнительное, [★] — повышенной сложности, [диаг.] — диагностическое.
Подготовка:
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 в образе.
Проверка:
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 [обяз.]
Постановка. Соберите образ, в котором:
ARGдоступен при сборке и недоступен в container.ENVдоступен и там, и там.- Значение, переданное через
--build-arg, попадает в переменную окружения работающего container.
Затем покажите, что секрет, переданный через --build-arg, извлекается из готового образа, и исправьте это через secret mount.
Ожидаемый результат. Демонстрация всех трёх пунктов и доказательство отсутствия секрета после исправления.
Проверка:
docker history --no-trunc <образ> | grep -o 'SECRET=[^ ]*' || echo "секрет не найден"
Разбор
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
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:
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
длина секрета: 20
--- извлечение из истории ---
SECRET=P@ssw0rd-Very-Secret
Исправление:
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
длина секрета: 20
--- поиск в истории ---
не найден
--- поиск в слоях ---
не найден в слоях
Результат сборки идентичен, но секрета в образе нет ни в метаданных, ни в файловой системе. Параметр required=true гарантирует, что при отсутствии секрета сборка упадёт явно, а не соберётся с пустым значением.
Задание 3. Поломка и починка кэша [обяз.]
Постановка. Соберите образ, намеренно сломайте кэш и по выводу сборки определите инструкцию-виновника.
- Соберите Python-приложение с
COPY . .передpip install. - Измерьте время пересборки после изменения одной строки кода.
- Определите по выводу, какая инструкция инвалидировалась первой.
- Исправьте порядок и измерьте снова.
Ожидаемый результат. Два замера с разницей не менее чем в 10 раз и указание точки каскада.
Проверка:
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.
Требования:
- Финальный образ минимум вдвое меньше.
- В финальном образе нет
gccиmake. - Приложение работает идентично.
- Есть общая базовая стадия без дублирования.
Ожидаемый результат. Два образа с замерами размера и подтверждение отсутствия компиляторов.
Проверка:
docker run --rm <образ> sh -c 'command -v gcc >/dev/null && echo есть || echo нет'
Полное решение — в уроке 5.7.
Задание 6. Cache mount [доп.]
Постановка. Добавьте cache mount для pip и измерьте эффект при изменении списка зависимостей.
Дополнительно докажите, что кэш не попадает в образ — сравните размеры образов с cache mount и без него.
Ожидаемый результат. Ускорение не менее чем в три раза при одинаковом размере образа.
Разбор
Полная реализация — в уроке 5.6. Ключевые моменты:
# 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 будет монтироваться, но оставаться пустым.
Проверка, что кэш не в образе:
docker run --rm <образ> ls /root/.cache/pip 2>&1 | tail -1
ls: cannot access '/root/.cache/pip': No such file or directory
Размеры образов с cache mount и без совпадают — данные живут в состоянии BuildKit, а не в слоях.
Задание 7. HEALTHCHECK с медленным стартом [доп.]
Постановка. Настройте healthcheck для приложения, стартующего 25 секунд, так чтобы:
- Container не помечался
unhealthyво время инициализации. - Готовность обнаруживалась в течение 3 секунд после её наступления.
- Отказ обнаруживался не дольше чем за 30 секунд.
Обоснуйте каждое выбранное значение параметра.
Ожидаемый результат. Подобранные параметры с обоснованием и лог переходов статуса.
Проверка:
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, где каждая сборка занимает около четырёх минут, хотя зависимости не менялись. Найдите три независимые причины и устраните каждую, измерив эффект по отдельности.
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.
Разбор
#!/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
Ожидаемый вывод:
═══ Пересборка после изменения одной строки кода ═══
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 не влияют на поведение.
FROM python:3.13-slim
COPY app.py /app.py
ENTRYPOINT python /app.py
CMD ["--default-mode"]
Требуется:
- Воспроизвести проблему.
- Объяснить механизм.
- Предложить два варианта исправления.
- Написать проверку, обнаруживающую эту проблему в чужом образе.
Разбор
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
=== 1. Воспроизведение ===
без аргументов: полученные аргументы: []
с аргументом: полученные аргументы: []
Оба раза пусто: CMD проигнорирован, аргумент проигнорирован.
2. Механизм.
docker image inspect ex510:broken --format 'Entrypoint: {{json .Config.Entrypoint}}'
docker image inspect ex510:broken --format 'Cmd: {{json .Config.Cmd}}'
Entrypoint: ["/bin/sh","-c","python /app.py"]
Cmd: ["--default-mode"]
ENTRYPOINT записан в shell form, поэтому превратился в /bin/sh -c "python /app.py".
Docker формирует итоговую команду конкатенацией Entrypoint + Cmd:
/bin/sh -c "python /app.py" "--default-mode"
Оболочка с флагом -c берёт первый аргумент как команду, а остальные становятся позиционными параметрами: $0 = --default-mode. Команда python /app.py их не использует — они теряются.
С аргументами docker run происходит то же самое: они попадают в ту же позицию.
3. Два варианта исправления.
Вариант A — exec form:
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
A без аргументов: полученные аргументы: ['--default-mode']
A с аргументом: полученные аргументы: ['--custom-mode']
Вариант B — shell form с exec "$@", если возможности оболочки всё же нужны:
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
B без аргументов: полученные аргументы: ['--default-mode']
B с аргументом: полученные аргументы: ['--custom-mode']
Оба варианта работают. Вариант A проще и предпочтителен, когда подготовка не нужна. Вариант B нужен, если перед запуском требуется что-то сделать — но тогда обязательны exec и "$@" в кавычках.
4. Проверка чужого образа.
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
[!] ENTRYPOINT записан в shell form.
Исправление: exec form либо exec "$@" в entrypoint-скрипте.
---
[+] Проблем с формой записи не обнаружено.
---
[+] Проблем с формой записи не обнаружено.
---
Скрипт возвращает ненулевой exit code при обнаружении проблемы, что делает его пригодным для CI: проверку можно включить в pipeline и не допускать публикации образов с этим дефектом.
Почему это важно проверять автоматически. Проблема молчаливая: сборка проходит, container запускается, приложение работает. Обнаруживается она обычно в момент, когда кто-то пытается передать параметр — часто уже в production.
Задание 11. Полная оптимизация [★]
Постановка. Дан Dockerfile реального Python-сервиса. Примените все приёмы раздела и доведите результат до целевых показателей.
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 секунд.
Решение
Сначала выполните задание самостоятельно.
Показать решение
Найденные проблемы.
| № | Проблема | Показатель | Урок |
|---|---|---|---|
| 1 | FROM python:latest | воспроизводимость | 5.9 |
| 2 | MAINTAINER устарел | — | 5.2 |
| 3 | ADD по URL без --checksum | воспроизводимость | 5.3 |
| 4 | Три RUN с apt-get раздельно | размер, cache busting | 5.3 |
| 5 | vim, git в образе | размер | 5.3 |
| 6 | Секрет через ARG → ENV | секреты | 5.2, 5.6 |
| 7 | COPY . /app до pip install | скорость пересборки | 5.5 |
| 8 | pytest, ruff в финальном образе | размер | 5.7 |
| 9 | Компиляторы в финальном образе | размер | 5.7 |
| 10 | RUN chmod и RUN chown -R | размер | 5.3 |
| 11 | USER nobody | безопасность | 5.2 |
| 12 | ENTRYPOINT в shell form | аргументы, docker stop | 5.4 |
| 13 | Нет HEALTHCHECK | наблюдаемость | 5.8 |
| 14 | Нет .dockerignore | контекст, скорость | 5.1 |
Итоговый 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:
#!/bin/sh
set -e
# подготовка при необходимости
exec python -m app.main "$@"
И .dockerignore:
.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 запускается явно:
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 |
| Секретов в образе | 1 | 0 |
| Компиляторов | есть | нет |
| Аргументы доходят | нет | да |
docker stop | 10.3 c | 0.4 c |
| Проверки блокируют сборку | нет (в образе) | да (стадия check) |
| Non-root с фиксированным UID | nobody | 10001 |
Проверочный набор команд:
# размер
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
Очистка после раздела
# ВНИМАНИЕ: НЕ `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
Главное оглавление