5.1. Build context и .dockerignore
Цели
После этого материала вы сможете:
- объяснить, что такое build context и что именно передаётся daemon при сборке;
- измерить размер контекста и найти каталоги, которые его раздувают;
- написать
.dockerignoreдля Python-проекта и обосновать каждую строку; - объяснить, как контекст влияет на время сборки, на build cache и на утечку файлов в образ;
- использовать
.dockerignoreдля конкретногоDockerfile; - собирать образ из stdin, из Git-репозитория и без контекста вовсе.
Предварительные знания
- Раздел 03. Работа с Images — слои и их формирование;
- Раздел 04. Containers и lifecycle;
- базовое представление о том, что
docker buildсобирает образ.
Ключевые термины
| Термин | Объяснение |
|---|---|
build context | Набор файлов, передаваемых daemon для сборки |
.dockerignore | Файл с шаблонами исключений из контекста |
frontend | Компонент BuildKit, интерпретирующий Dockerfile |
named context | Дополнительный именованный контекст, задаваемый флагом --build-context |
Git context | Сборка напрямую из репозитория без локального клона |
sending build context | Этап передачи файлов daemon, видимый в выводе legacy builder |
Теория
Что такое build context
Docker daemon не имеет доступа к вашей файловой системе. Он может работать на другой машине или, как минимум, является отдельным процессом с собственными правами.
Поэтому перед сборкой CLI собирает контекст — набор файлов из указанного каталога — и передаёт его daemon. Только эти файлы доступны инструкциям COPY и ADD.
docker build -t myapp .
↑
контекст: текущий каталог
Точка в конце — не «здесь лежит Dockerfile», а «вот каталог, содержимое которого нужно передать». Это разные вещи, и путаница между ними — источник ошибок.
ваша машина Docker daemon
┌────────────────────┐ ┌─────────────────────┐
│ ./ │ │ │
│ ├── Dockerfile │ ── контекст ►│ сборка использует │
│ ├── app/ │ (архив) │ только полученные │
│ ├── .venv/ ← 800 MB │ файлы │
│ └── .git/ ← 300 MB │ │
└────────────────────┘ └─────────────────────┘
Почему это важно
1. Время. Контекст передаётся при каждой сборке. Проект с .venv, .git и node_modules даёт контекст в сотни мегабайт, и на его передачу уходят секунды или минуты — до того, как выполнится первая инструкция.
2. Build cache. BuildKit вычисляет контрольную сумму файлов контекста. Изменение любого файла, попадающего под COPY . ., инвалидирует кэш этого слоя и всех последующих. Файл .pyc, созданный при локальном запуске тестов, приводит к пересборке зависимостей.
3. Утечка файлов в образ. COPY . . копирует всё, что попало в контекст. Файл .env с паролями, приватный ключ, дамп базы — всё окажется в слое образа и будет извлекаемо (урок 3.5).
4. Воспроизводимость. Локальные артефакты — кэш, скомпилированные файлы, конфигурация IDE — попадают в образ и делают сборку зависящей от состояния машины разработчика.
Что делает .dockerignore
Файл в корне контекста, перечисляющий шаблоны того, что не передавать. Работает аналогично .gitignore, но синтаксис отличается в деталях.
Правила синтаксиса:
| Шаблон | Значение |
|---|---|
__pycache__ | Совпадение по имени на любом уровне вложенности |
/build | Только в корне контекста |
*.pyc | Любой файл с расширением .pyc |
**/*.log | Любой .log на любом уровне (явная запись) |
temp? | Один произвольный символ: temp1, tempA |
!important.log | Исключение из исключения |
# комментарий | Строка игнорируется |
Порядок имеет значение: последнее совпавшее правило побеждает. Поэтому ! пишется после общего шаблона:
*.log
!errors.log ← сработает: идёт после
!errors.log
*.log ← errors.log всё равно исключён: правило ниже
Различие с .gitignore
Тонкость, на которой спотыкаются: в .dockerignore шаблон без слэша совпадает по имени на любом уровне, а в .gitignore поведение зависит от наличия слэша внутри шаблона. Практически это означает, что __pycache__ в .dockerignore исключит все такие каталоги на любой глубине без записи **/.
Второе различие: .dockerignore не поддерживает отдельные правила для каталогов через завершающий слэш так, как это делает Git. Запись build/ работает, но безопаснее писать build.
Что происходит при COPY . . с исключениями
Исключённые файлы просто отсутствуют в контексте. Для сборки их не существует: COPY не может их скопировать, RUN ls их не увидит.
Отсюда практическое следствие: если сборке нужен файл, он не должен быть исключён. Ошибка «COPY failed: file not found» при существующем файле почти всегда означает, что он попал под шаблон в .dockerignore.
Внутренний механизм
Как передаётся контекст
При использовании BuildKit (по умолчанию с Docker Engine 23.0) передача устроена эффективнее, чем в legacy builder.
Legacy builder упаковывал весь контекст в tar-архив и передавал целиком — отсюда знакомая строка Sending build context to Docker daemon 847.3MB.
BuildKit передаёт файлы по запросу: сначала передаётся список файлов с метаданными, а содержимое запрашивается только для тех, которые действительно нужны инструкциям COPY и ADD. Дополнительно работает кэширование: неизменившиеся файлы не передаются повторно.
Практическое следствие: BuildKit заметно быстрее на больших контекстах, но обход файловой системы всё равно происходит. Каталог .git на 300 MB с сотнями тысяч мелких файлов замедляет сборку даже при .dockerignore — правда, значительно меньше, чем без него.
Поэтому в выводе BuildKit вы видите шаг transferring context с указанием объёма:
=> [internal] load build context
=> => transferring context: 12.4kB
Где ищется .dockerignore
Файл ищется в корне контекста, а не рядом с Dockerfile. При сборке docker build -f docker/Dockerfile . будет использован ./.dockerignore, а не docker/.dockerignore.
Начиная с BuildKit есть возможность задать .dockerignore для конкретного Dockerfile: если рядом с файлом docker/api.Dockerfile лежит docker/api.Dockerfile.dockerignore, будет использован он. Это удобно для монорепозиториев с несколькими образами.
Команды и примеры
Измерение контекста
mkdir -p /tmp/ctx-demo && cd /tmp/ctx-demo
# структура типичного Python-проекта
mkdir -p app tests .git/objects .venv/lib/python3.13/site-packages __pycache__
cat > app/main.py <<'PY'
print("hello")
PY
cat > requirements.txt <<'EOF'
fastapi==0.141.1
EOF
# имитация тяжёлых каталогов
dd if=/dev/urandom of=.git/objects/pack.bin bs=1M count=40 status=none
dd if=/dev/urandom of=.venv/lib/python3.13/site-packages/lib.so bs=1M count=60 status=none
dd if=/dev/urandom of=__pycache__/main.cpython-313.pyc bs=1K count=200 status=none
echo "SECRET_KEY=production-secret-do-not-leak" > .env
cat > Dockerfile <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY . .
CMD ["python", "app/main.py"]
EOF
echo "=== размер каталога ==="
du -sh .
du -sh .git .venv __pycache__ app 2>/dev/null
=== размер каталога ===
101M .
41M .git
61M .venv
204K __pycache__
8.0K app
Полезного здесь — 8 килобайт. Остальные 100 мегабайт — мусор для сборки.
Сборка без .dockerignore:
docker build -t ctx:no-ignore . 2>&1 | grep -E 'transferring context|DONE' | head -3
=> => transferring context: 106.21MB 2.4s
Проверим, что попало в образ:
docker run --rm ctx:no-ignore ls -a /app
echo "--- секрет в образе? ---"
docker run --rm ctx:no-ignore cat /app/.env
.
..
.env
.git
.venv
__pycache__
app
Dockerfile
requirements.txt
tests
--- секрет в образе? ---
SECRET_KEY=production-secret-do-not-leak
В образ попали виртуальное окружение, история Git, кэш байт-кода и файл с секретом. Последнее — реальная утечка: секрет теперь в слое образа навсегда.
Размер образа:
docker images ctx:no-ignore --format '{{.Size}}'
228MB
Добавляем .dockerignore
cat > .dockerignore <<'EOF'
# Системы контроля версий
.git
.gitignore
# Виртуальные окружения
.venv
venv
env
# Кэш и артефакты Python
__pycache__
*.py[cod]
*.egg-info
.pytest_cache
.mypy_cache
.ruff_cache
.coverage
htmlcov
# Секреты и локальная конфигурация
.env
.env.*
*.pem
*.key
# Сам Docker
Dockerfile*
compose*.yaml
.dockerignore
# IDE и ОС
.idea
.vscode
.DS_Store
# Документация и CI
docs
README.md
.github
EOF
docker build -t ctx:with-ignore . 2>&1 | grep -E 'transferring context' | head -1
=> => transferring context: 1.87kB 0.0s
106 MB против 1.87 KB. Разница в 56 тысяч раз.
echo "--- содержимое образа ---"
docker run --rm ctx:with-ignore ls -a /app
echo "--- секрет? ---"
docker run --rm ctx:with-ignore cat /app/.env 2>&1 | tail -1
echo "--- размер ---"
docker images ctx:with-ignore --format '{{.Size}}'
--- содержимое образа ---
.
..
app
requirements.txt
tests
--- секрет? ---
cat: can't open '/app/.env': No such file or directory
--- размер ---
126MB
В образе только нужное, секрета нет, размер сократился со 228 до 126 MB.
Влияние на build cache
Это менее очевидный, но практически более важный эффект.
cd /tmp/ctx-demo
rm -f .dockerignore
cat > Dockerfile.layers <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app/main.py"]
EOF
# первая сборка — наполняем кэш
docker build -q -f Dockerfile.layers -t cache:test . > /dev/null
echo "=== пересборка без изменений ==="
docker build -f Dockerfile.layers -t cache:test . 2>&1 | grep -cE 'CACHED'
echo "=== имитируем локальный запуск тестов: появился .pyc ==="
dd if=/dev/urandom of=__pycache__/new.cpython-313.pyc bs=1K count=10 status=none
echo "=== пересборка после появления .pyc ==="
docker build -f Dockerfile.layers -t cache:test . 2>&1 | grep -E 'CACHED|RUN pip' | head -5
=== пересборка без изменений ===
4
=== имитируем локальный запуск тестов: появился .pyc ===
=== пересборка после появления .pyc ===
=> CACHED [2/5] WORKDIR /app
=> CACHED [3/5] COPY requirements.txt .
=> CACHED [4/5] RUN pip install --no-cache-dir -r requirements.txt
=> [5/5] COPY . .
Здесь кэш выдержал: COPY requirements.txt . идёт до COPY . ., поэтому установка зависимостей не пересобралась. Это правильный порядок инструкций, разбираемый в уроке 5.5.
Но если порядок неправильный:
cat > Dockerfile.bad <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "app/main.py"]
EOF
docker build -q -f Dockerfile.bad -t cache:bad . > /dev/null
dd if=/dev/urandom of=__pycache__/another.cpython-313.pyc bs=1K count=10 status=none
echo "=== пересборка при неправильном порядке ==="
docker build -f Dockerfile.bad -t cache:bad . 2>&1 | grep -E 'CACHED|RUN pip|COPY' | head -4
=== пересборка при неправильном порядке ===
=> CACHED [2/4] WORKDIR /app
=> [3/4] COPY . .
=> [4/4] RUN pip install --no-cache-dir -r requirements.txt
Установка зависимостей пересобралась — из-за файла .pyc, который вообще не имеет отношения к сборке. .dockerignore предотвращает это независимо от порядка инструкций.
Диагностика: что именно в контексте
Прямого способа «показать контекст» у Docker нет, но есть надёжный обходной приём — собрать образ, который просто копирует контекст и выводит его содержимое.
cd /tmp/ctx-demo
cat > Dockerfile.inspect <<'EOF'
FROM alpine:3.21
WORKDIR /ctx
COPY . .
CMD ["sh", "-c", "du -sh /ctx; echo '---'; find /ctx -type f | head -40"]
EOF
docker build -q -f Dockerfile.inspect -t ctx:inspect . > /dev/null
docker run --rm ctx:inspect
Без .dockerignore:
101.1M /ctx
---
/ctx/.env
/ctx/Dockerfile
/ctx/requirements.txt
/ctx/app/main.py
/ctx/.git/objects/pack.bin
/ctx/.venv/lib/python3.13/site-packages/lib.so
/ctx/__pycache__/main.cpython-313.pyc
...
Вернём .dockerignore и повторим:
cat > .dockerignore <<'EOF'
.git
.venv
__pycache__
*.py[cod]
.env
.env.*
Dockerfile*
.dockerignore
EOF
docker build -q -f Dockerfile.inspect -t ctx:inspect . > /dev/null
docker run --rm ctx:inspect
28.0K /ctx
---
/ctx/requirements.txt
/ctx/app/main.py
Это самый практичный способ проверить .dockerignore: вы видите ровно то, что видит сборка.
Правила исключений
mkdir -p /tmp/ignore-rules && cd /tmp/ignore-rules
mkdir -p logs sub/logs config
touch logs/app.log logs/errors.log sub/logs/deep.log config/app.conf keep.txt
cat > .dockerignore <<'EOF'
# исключить все .log на любом уровне
*.log
# но оставить errors.log
!errors.log
EOF
cat > Dockerfile <<'EOF'
FROM alpine:3.21
WORKDIR /ctx
COPY . .
CMD ["find", "/ctx", "-type", "f"]
EOF
docker build -q -t rules:1 . > /dev/null
docker run --rm rules:1
/ctx/config/app.conf
/ctx/keep.txt
/ctx/logs/errors.log
/ctx/.dockerignore
/ctx/Dockerfile
Разбор: *.log исключил все три файла с логами, !errors.log вернул один — на любом уровне, где он встретится.
Обратите внимание: .dockerignore и Dockerfile попали в контекст, потому что мы их не исключили. Обычно их стоит добавить в список — они не нужны внутри образа.
Проверим важность порядка:
cat > .dockerignore <<'EOF'
!errors.log
*.log
EOF
docker build -q -t rules:2 . > /dev/null
docker run --rm rules:2 | grep -c 'errors.log' || echo "errors.log исключён"
errors.log исключён
При обратном порядке !errors.log не срабатывает: последнее совпавшее правило — *.log.
.dockerignore действует на все стадии
Фильтр контекста один на всю сборку. Стадии его не переопределяют, и «своего» .dockerignore у стадии нет. Практически это значит: исключить можно только то, что не нужно ни одной стадии.
Ловушка возникает там, где multi-stage используется для прогона тестов (урок 5.7). Каталог tests в итоговом образе не нужен — рука сама тянется добавить его в .dockerignore. Но копирует его стадия test, и она перестаёт собираться:
mkdir -p /tmp/di-stages/app /tmp/di-stages/tests && cd /tmp/di-stages
echo "def add(a, b): return a + b" > app/lib.py
printf 'from app.lib import add\n\n\ndef test_add():\n assert add(2, 2) == 4\n' > tests/test_lib.py
cat > .dockerignore <<'EOF'
.git
.venv
__pycache__
tests
EOF
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS base
WORKDIR /app
COPY app/ ./app/
FROM base AS test
RUN pip install --quiet pytest==9.1.1
COPY tests/ ./tests/
RUN python -m pytest -q
FROM base AS runtime
CMD ["python", "-c", "from app.lib import add; print(add(2, 2))"]
EOF
echo "═══ стадия runtime ═══"
docker build -q --target runtime -t di:rt . > /dev/null && echo " собралась"
echo "═══ стадия test — тот же контекст, тот же .dockerignore ═══"
docker build --target test -t di:test .
═══ стадия runtime ═══
собралась
═══ стадия test — тот же контекст, тот же .dockerignore ═══
- CopyIgnoredFile: Attempting to Copy file "tests" that is excluded by .dockerignore (line 8)
ERROR: failed to compute cache key: "/tests": not found
Отказ отложенный: runtime собирается, и о поломке узнаёшь только когда доходишь до --target test — то есть в конвейере, а не на своей машине.
Число в скобках — строка Dockerfile, а не .dockerignore. Формулировка excluded by .dockerignore (line 8) читается наоборот, но в примере выше tests стоит в .dockerignore первой строкой, тогда как COPY tests/ ./tests/ — восьмая строка Dockerfile. Искать нужно по имени файла, а не по номеру.
Правильный ответ — не исключать tests:
sed -i '/^tests$/d' .dockerignore
docker build -q --target test -t di:test . > /dev/null && echo "стадия test собралась"
docker run --rm di:rt
docker run --rm --entrypoint sh di:rt -c 'ls /app'
стадия test собралась
4
app
В итоговом образе тестов нет — но не потому, что они исключены из контекста, а потому, что стадия runtime их не копирует. Именно COPY решает, что попадёт в образ; .dockerignore решает лишь, что вообще доедет до демона.
Правило: прежде чем добавить имя в .dockerignore, проверьте grep COPY Dockerfile — не копирует ли его какая-нибудь стадия.
cd /tmp && rm -rf /tmp/di-stages
docker rmi -f di:rt di:test > /dev/null 2>&1
.dockerignore для конкретного Dockerfile
Полезно в монорепозиториях, где из одного репозитория собираются разные образы.
mkdir -p /tmp/mono/docker && cd /tmp/mono
mkdir -p api worker shared
echo "print('api')" > api/main.py
echo "print('worker')" > worker/main.py
echo "SHARED = 1" > shared/common.py
cat > docker/api.Dockerfile <<'EOF'
FROM alpine:3.21
WORKDIR /ctx
COPY . .
CMD ["find", "/ctx", "-type", "f"]
EOF
# .dockerignore именно для api.Dockerfile
cat > docker/api.Dockerfile.dockerignore <<'EOF'
worker
docker
EOF
docker build -q -f docker/api.Dockerfile -t mono:api . > /dev/null
docker run --rm mono:api
/ctx/api/main.py
/ctx/shared/common.py
Каталог worker исключён: для образа API он не нужен. При сборке worker.Dockerfile действовал бы уже его собственный файл исключений.
Правило именования: <имя-dockerfile>.dockerignore рядом с самим Dockerfile. Если такого файла нет, используется .dockerignore из корня контекста.
Сборка без контекста и из других источников
Контекст не всегда нужен. Если Dockerfile не содержит COPY и ADD, передавать нечего.
Из stdin, без контекста:
docker build -t nocontext:1 - <<'EOF'
FROM alpine:3.21
RUN apk add --no-cache curl
CMD ["curl", "--version"]
EOF
docker run --rm nocontext:1 | head -1
curl 8.15.0 (x86_64-alpine-linux-musl) libcurl/8.15.0 ...
Дефис вместо пути означает «Dockerfile придёт со стандартного ввода, контекста нет». Сборка мгновенная — передавать нечего.
Dockerfile из stdin, контекст из каталога:
cd /tmp/ctx-demo
docker build -t combined:1 -f- . <<'EOF'
FROM alpine:3.21
WORKDIR /app
COPY requirements.txt .
CMD ["cat", "requirements.txt"]
EOF
docker run --rm combined:1
fastapi==0.141.1
Полезно для генерируемых Dockerfile — не нужно создавать временный файл.
Из Git-репозитория:
docker build -t fromgit:1 \
https://github.com/docker/welcome-to-docker.git#main 2>&1 | tail -2
Docker клонирует репозиторий на стороне сборки и использует его как контекст. Синтаксис: URL#ветка:подкаталог.
Локальный клон при этом не создаётся, а .dockerignore берётся из репозитория.
Именованные контексты
Возможность BuildKit: подключить дополнительный контекст под именем.
mkdir -p /tmp/named/{main,extra} && cd /tmp/named
echo "основной файл" > main/app.txt
echo "дополнительный файл" > extra/data.txt
cat > main/Dockerfile <<'EOF'
FROM alpine:3.21
WORKDIR /out
COPY . ./from-main/
COPY --from=extradata . ./from-extra/
CMD ["find", "/out", "-type", "f"]
EOF
docker build -q -t named:1 \
-f main/Dockerfile \
--build-context extradata=./extra \
./main > /dev/null
docker run --rm named:1
/out/from-main/Dockerfile
/out/from-main/app.txt
/out/from-extra/data.txt
Приём решает задачу «нужен файл вне каталога контекста» без копирования файлов и без расширения основного контекста. Классический случай — общая библиотека в монорепозитории, лежащая рядом с сервисом, а не внутри него.
Уборка
cd /tmp
docker rmi -f ctx:no-ignore ctx:with-ignore ctx:inspect cache:test cache:bad \
rules:1 rules:2 mono:api nocontext:1 combined:1 named:1 fromgit:1 2>/dev/null || true
rm -rf /tmp/ctx-demo /tmp/ignore-rules /tmp/mono /tmp/named
Практическое упражнение
Задание. Напишите скрипт context-audit.sh, который анализирует build context проекта и помогает составить .dockerignore.
Скрипт должен:
- Показать общий размер каталога и текущего контекста (с учётом существующего
.dockerignore). - Перечислить 10 самых больших каталогов и файлов.
- Отметить каталоги и файлы, которые почти наверняка не нужны в образе (
.git,.venv,__pycache__,node_modules,.env, ключи). - Предупредить о потенциальных секретах в контексте.
- Оценить, сколько освободит рекомендуемый
.dockerignore. - Сгенерировать готовый
.dockerignoreдля Python-проекта.
Скрипт ничего не должен изменять без явного подтверждения.
Подсказки
Подсказка 1
Фактический размер контекста удобно узнать через сборку временного образа, который копирует всё и печатает du -sh.
Подсказка 2
Самые большие каталоги:
du -sh */ .[!.]*/ 2>/dev/null | sort -hr | head -10
Подсказка 3
Поиск потенциальных секретов по именам файлов надёжнее, чем по содержимому: .env, *.pem, *.key, id_rsa, credentials*.
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/usr/bin/env bash
# context-audit.sh — аудит build context проекта.
set -uo pipefail
DIR="${1:-.}"
cd "$DIR" || { echo "Каталог не найден: $DIR" >&2; exit 1; }
echo "═══ Аудит build context: $(pwd) ═══"
echo
echo "── 1. Размеры ──"
total="$(du -sh . 2>/dev/null | cut -f1)"
printf ' Весь каталог: %s\n' "$total"
if [ -f .dockerignore ]; then
printf ' .dockerignore: есть (%s строк)\n' "$(grep -cvE '^\s*(#|$)' .dockerignore)"
else
printf ' .dockerignore: ОТСУТСТВУЕТ\n'
fi
# Фактический размер контекста — через пробную сборку
if docker info > /dev/null 2>&1; then
probe="ctxprobe-$$"
if docker build -q -t "$probe" -f- . > /dev/null 2>&1 <<'EOF'
FROM alpine:3.21
WORKDIR /ctx
COPY . .
CMD ["du","-sh","/ctx"]
EOF
then
actual="$(docker run --rm "$probe" 2>/dev/null | awk '{print $1}')"
printf ' Контекст: %s\n' "${actual:-?}"
docker rmi -f "$probe" > /dev/null 2>&1
fi
fi
echo
echo "── 2. Крупнейшие каталоги ──"
du -sh -- */ .[!.]*/ 2>/dev/null | sort -hr | head -10 | sed 's/^/ /'
echo
echo "── 3. Крупнейшие файлы ──"
find . -type f -size +1M 2>/dev/null \
-not -path './.git/*' -printf '%s\t%p\n' 2>/dev/null \
| sort -rn | head -5 \
| awk -F'\t' '{printf " %8.1f MB %s\n", $1/1048576, $2}'
echo
echo "── 4. Заведомо лишнее ──"
JUNK=(.git .venv venv env __pycache__ node_modules .pytest_cache
.mypy_cache .ruff_cache htmlcov dist build .tox .idea .vscode)
saved=0
found_junk=0
for j in "${JUNK[@]}"; do
if [ -e "$j" ]; then
sz="$(du -sk "$j" 2>/dev/null | cut -f1)"
printf ' %-20s %8.1f MB\n' "$j" "$(awk -v k="$sz" 'BEGIN{print k/1024}')"
saved=$((saved + sz))
found_junk=1
fi
done
[ "$found_junk" -eq 0 ] && echo " не найдено"
echo
echo "── 5. Возможные секреты ──"
secrets=0
while IFS= read -r file; do
[ -z "$file" ] && continue
printf ' [!] %s\n' "$file"
secrets=$((secrets + 1))
done < <(find . -maxdepth 3 \( \
-name '.env' -o -name '.env.*' -o -name '*.pem' -o -name '*.key' \
-o -name 'id_rsa*' -o -name 'credentials*' -o -name '*.p12' \
\) -not -path './.git/*' 2>/dev/null)
if [ "$secrets" -eq 0 ]; then
echo " [+] не найдено"
else
echo
echo " Эти файлы попадут в образ при 'COPY . .' и будут"
echo " извлекаемы из слоёв. Исключите их в .dockerignore."
fi
echo
echo "── 6. Оценка выигрыша ──"
if [ "$saved" -gt 0 ]; then
printf ' Рекомендуемый .dockerignore уберёт ~%.1f MB из контекста\n' \
"$(awk -v k="$saved" 'BEGIN{print k/1024}')"
else
echo " Контекст уже компактный"
fi
echo
echo "── 7. Рекомендуемый .dockerignore ──"
cat <<'IGNORE' | sed 's/^/ /'
# Системы контроля версий
.git
.gitignore
.gitattributes
# Виртуальные окружения
.venv
venv
env
ENV
# Кэш и артефакты Python
__pycache__
*.py[cod]
*$py.class
*.egg-info
.eggs
dist
build
.pytest_cache
.mypy_cache
.ruff_cache
.tox
.coverage
.coverage.*
htmlcov
# Секреты и локальная конфигурация
.env
.env.*
!.env.example
*.pem
*.key
*.p12
id_rsa*
credentials*
# Docker
Dockerfile*
compose*.yaml
compose*.yml
.dockerignore
# IDE и ОС
.idea
.vscode
*.swp
.DS_Store
Thumbs.db
# Документация и CI
docs
*.md
!README.md
.github
.gitlab-ci.yml
IGNORE
echo
if [ ! -f .dockerignore ]; then
read -r -p "Создать .dockerignore с этим содержимым? [y/N] " ans
if [ "${ans:-n}" = "y" ]; then
"$0" --emit-ignore > .dockerignore 2>/dev/null \
|| echo "Скопируйте блок выше вручную."
echo "Готово."
fi
else
echo " .dockerignore уже существует — сравните с рекомендуемым вручную."
fi
Проверка:
mkdir -p /tmp/audit-test && cd /tmp/audit-test
mkdir -p .git .venv __pycache__ app
dd if=/dev/urandom of=.git/pack bs=1M count=30 status=none
dd if=/dev/urandom of=.venv/lib.so bs=1M count=50 status=none
echo "SECRET=x" > .env
echo "print(1)" > app/main.py
chmod +x context-audit.sh
./context-audit.sh
Ожидаемый вывод (сокращённо):
═══ Аудит build context: /tmp/audit-test ═══
── 1. Размеры ──
Весь каталог: 81M
.dockerignore: ОТСУТСТВУЕТ
Контекст: 80.1M
── 2. Крупнейшие каталоги ──
50M .venv/
30M .git/
4.0K app/
── 3. Крупнейшие файлы ──
50.0 MB ./.venv/lib.so
── 4. Заведомо лишнее ──
.git 30.0 MB
.venv 50.0 MB
__pycache__ 0.0 MB
── 5. Возможные секреты ──
[!] ./.env
Эти файлы попадут в образ при 'COPY . .' и будут
извлекаемы из слоёв. Исключите их в .dockerignore.
── 6. Оценка выигрыша ──
Рекомендуемый .dockerignore уберёт ~80.0 MB из контекста
...
Что делает аудит полезным.
Пункт 1 показывает фактический размер контекста через пробную сборку, а не оценку по du. Разница существенна, когда .dockerignore уже частично написан: du покажет 500 MB, а контекст окажется 2 MB, и оптимизировать нечего.
Пункт 5 — самый важный. Файл .env в контексте означает, что при COPY . . секреты попадут в слой образа и останутся там навсегда: удалить их последующей инструкцией нельзя (урок 3.2). Проверка занимает секунду и предотвращает утечку, которая иначе обнаружится после публикации образа.
Строка !.env.example в рекомендуемом файле не случайна: пример конфигурации без реальных значений обычно нужен в образе или хотя бы полезен в репозитории, и общий шаблон .env.* его бы исключил.
Проверка результата
mkdir -p /tmp/check && cd /tmp/check
mkdir -p .venv && dd if=/dev/urandom of=.venv/big bs=1M count=20 status=none
echo "print(1)" > app.py
printf 'FROM alpine:3.21\nWORKDIR /c\nCOPY . .\nCMD ["du","-sh","/c"]\n' > Dockerfile
echo "без .dockerignore:"
docker build -q -t chk:1 . > /dev/null && docker run --rm chk:1
echo ".venv" > .dockerignore
echo "с .dockerignore:"
docker build -q -t chk:2 . > /dev/null && docker run --rm chk:2
docker rmi -f chk:1 chk:2 > /dev/null; cd /tmp && rm -rf /tmp/check
Разница должна составлять около 20 MB.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
Нет .dockerignore в Python-проекте | Не задумывались | .venv и .git попадают в контекст и в образ |
.env попал в образ | COPY . . без исключений | Исключить .env и .env.*; секрет из слоя уже не убрать |
.dockerignore рядом с Dockerfile, а не в корне контекста | Логично, но неверно | Файл ищется в корне контекста; либо использовать <dockerfile>.dockerignore |
!file перед общим шаблоном | Порядок кажется неважным | Побеждает последнее совпавшее правило; исключение пишется после |
COPY failed: file not found при существующем файле | Файл исключён шаблоном | Проверить .dockerignore пробной сборкой |
| Долгая сборка при маленьком проекте | Тяжёлые каталоги в контексте | Измерить контекст, добавить исключения |
| Пересборка зависимостей при изменении кода | .pyc и кэш в контексте инвалидируют слой | .dockerignore плюс правильный порядок инструкций |
| Копирование файлов извне проекта в контекст «чтобы собралось» | Не знают про именованные контексты | --build-context name=./path и COPY --from=name |
Dockerfile внутри образа | Не исключён | Добавить Dockerfile* в .dockerignore |
Контрольные вопросы
На понимание:
- Что означает точка в конце команды
docker build -t app .? - Почему daemon не может просто прочитать файлы из вашего каталога?
- Как BuildKit сокращает передачу контекста и почему
.dockerignoreвсё равно нужен? - Почему файл
.pycв контексте может привести к пересборке зависимостей? - Почему секрет, попавший в образ через
COPY . ., нельзя удалить последующей инструкцией?
На применение:
- Как узнать, что именно попало в контекст сборки?
- Как задать разные исключения для двух
Dockerfileв одном репозитории? - Как использовать при сборке файл, лежащий вне каталога контекста?
На диагностику:
COPY app/config.yaml .завершается с ошибкой «file not found», хотя файл на месте. Причина?- Сборка простого Python-проекта занимает две минуты, из них полторы — до первой инструкции. Что проверить?
Краткое резюме
- Build context — набор файлов, передаваемых daemon; только они доступны инструкциям
COPYиADD. - Точка в
docker build .задаёт каталог контекста, а не расположениеDockerfile. - Контекст влияет на время сборки, build cache, размер образа и утечку файлов.
.dockerignoreищется в корне контекста, а не рядом сDockerfile.- Шаблон без слэша совпадает по имени на любом уровне вложенности.
- Побеждает последнее совпавшее правило — исключения через
!пишутся после общего шаблона. - BuildKit передаёт файлы по запросу, но обход файловой системы всё равно происходит.
- Для конкретного
Dockerfileможно задать<имя>.dockerignoreрядом с ним. - Проверить содержимое контекста надёжнее всего пробной сборкой с
COPY . .иfind. - Именованные контексты (
--build-context) дают доступ к файлам вне основного каталога.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Build context | https://docs.docker.com/build/concepts/context/ | Что такое контекст, сборка из stdin, из Git, именованные контексты |
| .dockerignore file | https://docs.docker.com/build/concepts/context/#dockerignore-files | Синтаксис шаблонов, правило последнего совпадения, расположение файла |
| docker build reference | https://docs.docker.com/reference/cli/docker/buildx/build/ | Флаги -f, --build-context, сборка из stdin |
| Building best practices | https://docs.docker.com/build/building/best-practices/ | Рекомендация исключать ненужное из контекста |
| Build cache | https://docs.docker.com/build/cache/ | Влияние изменений контекста на инвалидацию кэша |
| Dockerfile reference: COPY | https://docs.docker.com/reference/dockerfile/#copy | Доступность файлов только из контекста |
| BuildKit | https://docs.docker.com/build/buildkit/ | Передача файлов по запросу вместо целого архива |
Навигация
Вернуться к разделу
Следующий материал → Базовые инструкции
Главное оглавление