Главная/Dockerfile/Урок

5.1. Build context и .dockerignore

Цели

После этого материала вы сможете:

  • объяснить, что такое build context и что именно передаётся daemon при сборке;
  • измерить размер контекста и найти каталоги, которые его раздувают;
  • написать .dockerignore для Python-проекта и обосновать каждую строку;
  • объяснить, как контекст влияет на время сборки, на build cache и на утечку файлов в образ;
  • использовать .dockerignore для конкретного Dockerfile;
  • собирать образ из stdin, из Git-репозитория и без контекста вовсе.

Предварительные знания

Ключевые термины

ТерминОбъяснение
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.

bash
docker build -t myapp .
                     ↑
                     контекст: текущий каталог

Точка в конце — не «здесь лежит Dockerfile», а «вот каталог, содержимое которого нужно передать». Это разные вещи, и путаница между ними — источник ошибок.

text
   ваша машина                          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Исключение из исключения
# комментарийСтрока игнорируется

Порядок имеет значение: последнее совпавшее правило побеждает. Поэтому ! пишется после общего шаблона:

text
*.log
!errors.log       ← сработает: идёт после
text
!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 с указанием объёма:

text
 => [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, будет использован он. Это удобно для монорепозиториев с несколькими образами.


Команды и примеры

Измерение контекста

bash
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
text
=== размер каталога ===
101M	.
41M	.git
61M	.venv
204K	__pycache__
8.0K	app

Полезного здесь — 8 килобайт. Остальные 100 мегабайт — мусор для сборки.

Сборка без .dockerignore:

bash
docker build -t ctx:no-ignore . 2>&1 | grep -E 'transferring context|DONE' | head -3
text
 => => transferring context: 106.21MB                                     2.4s

Проверим, что попало в образ:

bash
docker run --rm ctx:no-ignore ls -a /app
echo "--- секрет в образе? ---"
docker run --rm ctx:no-ignore cat /app/.env
text
.
..
.env
.git
.venv
__pycache__
app
Dockerfile
requirements.txt
tests
--- секрет в образе? ---
SECRET_KEY=production-secret-do-not-leak

В образ попали виртуальное окружение, история Git, кэш байт-кода и файл с секретом. Последнее — реальная утечка: секрет теперь в слое образа навсегда.

Размер образа:

bash
docker images ctx:no-ignore --format '{{.Size}}'
text
228MB

Добавляем .dockerignore

bash
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
text
 => => transferring context: 1.87kB                                       0.0s

106 MB против 1.87 KB. Разница в 56 тысяч раз.

bash
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}}'
text
--- содержимое образа ---
.
..
app
requirements.txt
tests
--- секрет? ---
cat: can't open '/app/.env': No such file or directory
--- размер ---
126MB

В образе только нужное, секрета нет, размер сократился со 228 до 126 MB.

Влияние на build cache

Это менее очевидный, но практически более важный эффект.

bash
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
text
=== пересборка без изменений ===
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.

Но если порядок неправильный:

bash
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
text
=== пересборка при неправильном порядке ===
 => CACHED [2/4] WORKDIR /app
 => [3/4] COPY . .
 => [4/4] RUN pip install --no-cache-dir -r requirements.txt

Установка зависимостей пересобралась — из-за файла .pyc, который вообще не имеет отношения к сборке. .dockerignore предотвращает это независимо от порядка инструкций.

Диагностика: что именно в контексте

Прямого способа «показать контекст» у Docker нет, но есть надёжный обходной приём — собрать образ, который просто копирует контекст и выводит его содержимое.

bash
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:

text
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 и повторим:

bash
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
text
28.0K	/ctx
---
/ctx/requirements.txt
/ctx/app/main.py

Это самый практичный способ проверить .dockerignore: вы видите ровно то, что видит сборка.

Правила исключений

bash
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
text
/ctx/config/app.conf
/ctx/keep.txt
/ctx/logs/errors.log
/ctx/.dockerignore
/ctx/Dockerfile

Разбор: *.log исключил все три файла с логами, !errors.log вернул один — на любом уровне, где он встретится.

Обратите внимание: .dockerignore и Dockerfile попали в контекст, потому что мы их не исключили. Обычно их стоит добавить в список — они не нужны внутри образа.

Проверим важность порядка:

bash
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 исключён"
text
errors.log исключён

При обратном порядке !errors.log не срабатывает: последнее совпавшее правило — *.log.

.dockerignore действует на все стадии

Фильтр контекста один на всю сборку. Стадии его не переопределяют, и «своего» .dockerignore у стадии нет. Практически это значит: исключить можно только то, что не нужно ни одной стадии.

Ловушка возникает там, где multi-stage используется для прогона тестов (урок 5.7). Каталог tests в итоговом образе не нужен — рука сама тянется добавить его в .dockerignore. Но копирует его стадия test, и она перестаёт собираться:

bash
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 .
text
═══ стадия 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:

bash
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'
text
стадия test собралась
4
app

В итоговом образе тестов нет — но не потому, что они исключены из контекста, а потому, что стадия runtime их не копирует. Именно COPY решает, что попадёт в образ; .dockerignore решает лишь, что вообще доедет до демона.

Правило: прежде чем добавить имя в .dockerignore, проверьте grep COPY Dockerfile — не копирует ли его какая-нибудь стадия.

bash
cd /tmp && rm -rf /tmp/di-stages
docker rmi -f di:rt di:test > /dev/null 2>&1

.dockerignore для конкретного Dockerfile

Полезно в монорепозиториях, где из одного репозитория собираются разные образы.

bash
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
text
/ctx/api/main.py
/ctx/shared/common.py

Каталог worker исключён: для образа API он не нужен. При сборке worker.Dockerfile действовал бы уже его собственный файл исключений.

Правило именования: <имя-dockerfile>.dockerignore рядом с самим Dockerfile. Если такого файла нет, используется .dockerignore из корня контекста.

Сборка без контекста и из других источников

Контекст не всегда нужен. Если Dockerfile не содержит COPY и ADD, передавать нечего.

Из stdin, без контекста:

bash
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
text
curl 8.15.0 (x86_64-alpine-linux-musl) libcurl/8.15.0 ...

Дефис вместо пути означает «Dockerfile придёт со стандартного ввода, контекста нет». Сборка мгновенная — передавать нечего.

Dockerfile из stdin, контекст из каталога:

bash
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
text
fastapi==0.141.1

Полезно для генерируемых Dockerfile — не нужно создавать временный файл.

Из Git-репозитория:

bash
docker build -t fromgit:1 \
    https://github.com/docker/welcome-to-docker.git#main 2>&1 | tail -2

Docker клонирует репозиторий на стороне сборки и использует его как контекст. Синтаксис: URL#ветка:подкаталог.

Локальный клон при этом не создаётся, а .dockerignore берётся из репозитория.

Именованные контексты

Возможность BuildKit: подключить дополнительный контекст под именем.

bash
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
text
/out/from-main/Dockerfile
/out/from-main/app.txt
/out/from-extra/data.txt

Приём решает задачу «нужен файл вне каталога контекста» без копирования файлов и без расширения основного контекста. Классический случай — общая библиотека в монорепозитории, лежащая рядом с сервисом, а не внутри него.

Уборка

bash
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.

Скрипт должен:

  1. Показать общий размер каталога и текущего контекста (с учётом существующего .dockerignore).
  2. Перечислить 10 самых больших каталогов и файлов.
  3. Отметить каталоги и файлы, которые почти наверняка не нужны в образе (.git, .venv, __pycache__, node_modules, .env, ключи).
  4. Предупредить о потенциальных секретах в контексте.
  5. Оценить, сколько освободит рекомендуемый .dockerignore.
  6. Сгенерировать готовый .dockerignore для Python-проекта.

Скрипт ничего не должен изменять без явного подтверждения.

Подсказки

Подсказка 1

Фактический размер контекста удобно узнать через сборку временного образа, который копирует всё и печатает du -sh.

Подсказка 2

Самые большие каталоги:

bash
du -sh */ .[!.]*/ 2>/dev/null | sort -hr | head -10
Подсказка 3

Поиск потенциальных секретов по именам файлов надёжнее, чем по содержимому: .env, *.pem, *.key, id_rsa, credentials*.

Решение

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

Показать решение
bash
#!/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

Проверка:

bash
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

Ожидаемый вывод (сокращённо):

text
═══ Аудит 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.* его бы исключил.

Проверка результата

bash
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

Контрольные вопросы

На понимание:

  1. Что означает точка в конце команды docker build -t app .?
  2. Почему daemon не может просто прочитать файлы из вашего каталога?
  3. Как BuildKit сокращает передачу контекста и почему .dockerignore всё равно нужен?
  4. Почему файл .pyc в контексте может привести к пересборке зависимостей?
  5. Почему секрет, попавший в образ через COPY . ., нельзя удалить последующей инструкцией?

На применение:

  1. Как узнать, что именно попало в контекст сборки?
  2. Как задать разные исключения для двух Dockerfile в одном репозитории?
  3. Как использовать при сборке файл, лежащий вне каталога контекста?

На диагностику:

  1. COPY app/config.yaml . завершается с ошибкой «file not found», хотя файл на месте. Причина?
  2. Сборка простого Python-проекта занимает две минуты, из них полторы — до первой инструкции. Что проверить?

Краткое резюме

  1. Build context — набор файлов, передаваемых daemon; только они доступны инструкциям COPY и ADD.
  2. Точка в docker build . задаёт каталог контекста, а не расположение Dockerfile.
  3. Контекст влияет на время сборки, build cache, размер образа и утечку файлов.
  4. .dockerignore ищется в корне контекста, а не рядом с Dockerfile.
  5. Шаблон без слэша совпадает по имени на любом уровне вложенности.
  6. Побеждает последнее совпавшее правило — исключения через ! пишутся после общего шаблона.
  7. BuildKit передаёт файлы по запросу, но обход файловой системы всё равно происходит.
  8. Для конкретного Dockerfile можно задать <имя>.dockerignore рядом с ним.
  9. Проверить содержимое контекста надёжнее всего пробной сборкой с COPY . . и find.
  10. Именованные контексты (--build-context) дают доступ к файлам вне основного каталога.

Официальные источники

ИсточникСсылкаЧто подтверждает
Build contexthttps://docs.docker.com/build/concepts/context/Что такое контекст, сборка из stdin, из Git, именованные контексты
.dockerignore filehttps://docs.docker.com/build/concepts/context/#dockerignore-filesСинтаксис шаблонов, правило последнего совпадения, расположение файла
docker build referencehttps://docs.docker.com/reference/cli/docker/buildx/build/Флаги -f, --build-context, сборка из stdin
Building best practiceshttps://docs.docker.com/build/building/best-practices/Рекомендация исключать ненужное из контекста
Build cachehttps://docs.docker.com/build/cache/Влияние изменений контекста на инвалидацию кэша
Dockerfile reference: COPYhttps://docs.docker.com/reference/dockerfile/#copyДоступность файлов только из контекста
BuildKithttps://docs.docker.com/build/buildkit/Передача файлов по запросу вместо целого архива

Навигация

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

Markdown на GitHub ↗