Ключевое отличие COPY от ADD: что нужно знать сразу
COPY переносит файлы и каталоги из контекста сборки в файловую систему образа и на этом останавливается. ADD делает то же самое, но добавляет два неявных действия: разворачивает локальные tar-архивы (включая сжатые gzip, bzip2 и xz) и скачивает файлы по HTTP/HTTPS.
Прямой ответ на главный вопрос: в 2026 году по умолчанию используйте COPY. ADD оправдан в редких случаях, когда нужно распаковать локальный архив, содержимое которого вы контролируете. Официальная документация Docker в разделе Building best practices содержит отдельные ссылки на справочные страницы инструкций ADD и COPY, что подтверждает рассмотрение обеих инструкций в контексте рекомендаций по сборке (Building best practices | Docker Docs). Прямого указания «COPY предпочтительнее» и правила hadolint DL3020 в доступных источниках нет, поэтому относитесь к ним как к распространённой практике, а не как к цитате из документации.
Разница видна на двух строках. COPY . /app положит в /app ровно содержимое контекста сборки. ADD app.tar.gz /app развернёт архив и создаст внутри /app структуру каталогов из него; перечень файлов в Dockerfile не виден, и узнать его можно только по результату сборки.
Почему ADD считается менее предсказуемым
Распаковка в ADD срабатывает автоматически, если источник - локальный tar-архив в распознаваемом формате сжатия. Отдельного переключателя «не распаковывать» нет: чтобы положить архив в образ как файл, придётся либо переименовать его в контексте, либо использовать COPY. Итоговая структура каталогов определяется содержимым архива, а не строкой в Dockerfile, поэтому такой ADD сложнее ревьюить и отлаживать.
Загрузка по URL привязывает сборку к внешнему ресурсу и сети. Строка вида ADD https://example.com/file.txt /app/file.txt упадёт, если сервер недоступен или отдаёт ошибку, а сам файл получит права 600 по умолчанию (dockerlabs: ADD-command.md). Утверждение про владельца root и про ADD --checksum=sha256 как единственный способ фиксации содержимого в доступных источниках не подтверждено, поэтому не полагайтесь на них как на гарантию: если нужна воспроизводимость, скачивайте через RUN с явной проверкой хеша.
Для скачивания во время сборки используйте RUN с curl или wget. Тогда в Dockerfile явно видны адрес, флаги проверки сертификата, распаковка и удаление временных файлов, а всё это укладывается в один слой.
Синтаксис и параметры COPY и ADD
Базовый вид одинаков для обеих инструкций: COPY <src>... <dest> и ADD <src>... <dest>. Источники указываются относительно корня контекста сборки, поддерживаются wildcard-шаблоны и несколько источников сразу. Если источник - каталог, копируется его содержимое вместе с метаданными файлов; сама директория как вложенная не появляется.
Ведёт себя по-разному приёмник. Когда <dest> не заканчивается слешем и такого пути в образе нет, Docker считает его файлом: COPY app.conf /etc/nginx.conf создаст именно файл. Если источников несколько или использован wildcard, приёмник обязан быть каталогом и заканчиваться слешем, иначе сборка завершится ошибкой (The Unexpected Importance of the Trailing Slash).
Из флагов чаще всего нужны четыре. --chown задаёт владельца скопированных файлов, --chmod выставляет режим доступа (например, --chmod=755), --from берёт файлы из другого этапа multi-stage сборки, --link добавляет слой независимо от предыдущих инструкций и ускоряет пересборку в CI. COPY --link доступен только при использовании BuildKit и требует включения синтаксиса Dockerfile v1.4 через комментарий в начале файла (How to Accelerate Docker Builds With COPY --link, Как ускорить сборку Docker и оптимизировать кэширование). Про флаг --exclude в доступных источниках подтверждений нет, поэтому не рассчитывайте на него без проверки в вашей версии сборщика.
Флаг --chown: как избежать проблем с правами
По умолчанию скопированные файлы принадлежат root (UID 0, GID 0). Пока контейнер запускается от root, это незаметно. Как только в Dockerfile появляется USER app, приложение получает ошибку доступа к /app при попытке записать лог или временный файл.
Решение описывается в той же инструкции: COPY --chown=app:app . /app. Если пользователя в образе нет (например, базовый distroless), указывайте числа: COPY --chown=1001:1001 . /app. Числовой вариант предсказуемее, поскольку не зависит от наличия записи в /etc/passwd. Флаг применяется ко всем источникам одной инструкции, поэтому смешивать в одном COPY файлы с разными владельцами не получится, их придётся разнести по строкам. ADD --chown тоже поддерживает, но это не повод выбирать ADD.
Отдельная инструкция RUN chown -R app:app /app работает, но обходится дороже: изменённые файлы целиком попадают в новый слой, и образ растёт на размер всего каталога. --chown выставляет владельца в момент создания слоя, без дублирования данных. Подробнее о запуске контейнера без root и о безопасных шаблонах Dockerfile мы разбирали в материале Dockerfile 2026: безопасные образы для Python, Node.js и Go.
Когда ADD действительно оправдан: сценарии использования
Единственный регулярный сценарий - развернуть локальный tar-архив без отдельной инструкции. ADD app.tar.gz /app/ распакует архив в /app и не оставит сам архив в образе. Эквивалент: COPY app.tar.gz /tmp/app.tar.gz, затем RUN tar -xzf /tmp/app.tar.gz -C /app && rm /tmp/app.tar.gz.
Вариант с COPY плюс RUN даёт контроль: вы видите команду распаковки, получаете понятную ошибку в логе, если архив повреждён, и удаляете архив в том же слое, чтобы он не занимал место. При использовании ADD вы доверяете разбор архива сборщику и не можете повлиять на результат, если внутри окажется лишний каталог.
Второй редкий случай - скачивание внешнего артефакта по URL. Идея с ADD --checksum=sha256: https://example.com/artifact.tar.gz /opt/ выглядит привлекательно, когда файл физически нельзя положить в контекст сборки, но в доступных источниках подтверждения этого флага нет. Если воспроизводимость критична, скачивайте через RUN curl с явной проверкой контрольной суммы.
Всё остальное - распаковка архивов из недоверенных источников, ADD . /app в проекте, где в контексте лежит случайный .tar.gz, загрузка по URL без проверки целостности - не те задачи, ради которых стоит усложнять Dockerfile.
Практические примеры: копирование исходников, конфигов и статики
Исходники. Разделяйте редко меняющиеся файлы зависимостей и часто меняющийся код, чтобы кэш не сбрасывался на каждой правке:
FROM node:22-alpine WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY . .
Первый COPY инвалидируется только при изменении манифестов, второй - при любой правке кода. Рядом обязателен .dockerignore: без него в контекст попадут node_modules, .git и локальные .env, а копирование займёт больше времени.
Конфиги. Для одного файла удобно копирование «файл в файл»: COPY nginx.conf /etc/nginx/nginx.conf. Для набора конфигов копируйте содержимое каталога: COPY ./conf.d/ /etc/nginx/conf.d/. Если приёмник не существует и не заканчивается слешем, Docker создаст файл с таким именем, и nginx не увидит конфигурацию. Проверить результат заранее: docker run --rm <image> nginx -T.
Статика в multi-stage сборке. В финальный образ переносится только результат сборки, без компиляторов и node_modules:
FROM node:22-alpine AS builder WORKDIR /app COPY . . RUN npm ci && npm run build FROM nginx:1.27-alpine COPY --from=builder /app/dist /usr/share/nginx/html
Здесь COPY --from=builder читает каталог из этапа builder и пишет его в финальный образ. Для слойного кэша и порядка инструкций в таких сборках есть отдельный разбор с командами проверки: сборка Docker-образа пошагово: Dockerfile, кэширование, multi-stage build.
Копирование директорий: тонкости с trailing slash
Ожидание из rsync здесь не работает: слеш в конце источника на результат COPY не влияет. COPY ./config /app/config и COPY ./config/ /app/config кладут в /app/config содержимое каталога config; вложенная папка /app/config/config не появится. Проверить можно одной командой: docker run --rm <image> find /app -maxdepth 2.
Значение имеет слеш в приёмнике и количество источников. Если путь не существует и слеша нет, Docker создаёт файл: COPY nginx.conf /etc/nginx/ создаст /etc/nginx/nginx.conf, а COPY nginx.conf /etc/nginx создаст файл с именем nginx (затрёт или создаст каталог-конфликт, если такой путь уже есть в базовом образе). Два источника в одном приёмнике без завершающего слеша дадут ошибку сборки: при нескольких источниках или wildcard приёмник обязан быть каталогом и заканчиваться слешем (The Unexpected Importance of the Trailing Slash).
Ещё два момента при копировании каталогов. COPY заменяет существующий файл целиком, а не дописывает его, поэтому правки в базовом образе не сохраняются. И .dockerignore фильтрует контекст до старта сборки: строки node_modules, .git, *.log уменьшают размер контекста и время передачи его сборщику.
Типичные ошибки при использовании COPY и ADD
- ADD вместо COPY без необходимости. Самая частая причина сюрпризов: ADD . /app может распаковать случайный tar-архив из контекста, и файлы окажутся в образе там, где вы их не ждёте. Исправление: заменить на COPY, а распаковку описать явно.
- Нет .dockerignore. В образ уезжают .git, node_modules, тестовые дампы и .env с токенами. Это и лишние десятки мегабайт, и потенциальная утечка секретов. Добавьте файл в корень контекста перед первой сборкой.
- Неверный порядок COPY и RUN. Если копировать весь код до установки зависимостей, любая правка строки в приложении сбрасывает кэш установки пакетов и сборка удлиняется на минуты. Копируйте lock-файлы, ставьте зависимости, и только потом копируйте остальное.
- ADD с URL. Сборка зависит от доступности внешнего хоста, а файл получает права 600 по умолчанию. Замените на RUN curl с явными флагами и проверкой хеша.
- Отсутствие --chown. Сборка проходит, а контейнер падает на первом же обращении к файлу при запуске от непривилегированного пользователя. Ошибка видна только в рантайме, поэтому проверяйте права после сборки: docker run --rm --user 1001 <image> ls -la /app.
- RUN chown -R в отдельном слое. Дублирует все файлы каталога в новом слое и раздувает образ. Меняйте владельца через --chown в самой инструкции копирования. Как искать такие «тяжёлые» слои, описано в руководстве по анализу слоёв образа с docker inspect и dive.
- Секреты внутри образа. Ключи, .npmrc с токеном реестра и приватные сертификаты, скопированные через COPY, остаются в слоях навсегда, даже если удалить их следующей инструкцией. Передавайте их через секреты сборки, а общий чек-лист защиты контейнеров есть в материале полное руководство по безопасности Docker-контейнеров.
Рекомендации для продакшена в 2026 году
Практическое правило: COPY по умолчанию, ADD только для локального tar-архива под вашим контролем. COPY делает Dockerfile читаемым, результат сборки предсказуемым, а поведение слоёв воспроизводимым на любой машине сборщика. Это рекомендация из практики, а не цитата из документации: в доступных источниках прямого указания «COPY предпочтительнее» и отдельного статуса ADD в Dockerfile на 2026 год нет.
Для CI добавьте COPY --link: слой формируется независимо от предыдущих инструкций, и пересборка при изменении только кода проходит быстрее. Флаг требует BuildKit и включения синтаксиса Dockerfile v1.4 через комментарий в начале файла. Если артефакт нужно взять из другого этапа, используйте COPY --from=, а не копирование всего контекста в финальный образ.
Чек-лист перед коммитом Dockerfile:
- Проверьте, нужна ли распаковка архива. Не нужна - пишите COPY.
- Нужна - берите COPY плюс RUN tar -xzf с удалением архива в том же слое.
- Указывайте --chown, в distroless - числами (1001:1001).
- Держите .dockerignore с .git, node_modules, .env, *.log.
- Ставьте редко меняющиеся файлы (lock-манифесты) до COPY всего кода.
- Прогоните линтер: правило DL3020 в hadolint ловит ADD там, где достаточно COPY (проверьте актуальность правила в вашей версии линтера).
Сравнительная таблица COPY и ADD
| Критерий | COPY | ADD | Комментарий |
|---|---|---|---|
| Копирование файлов и каталогов из контекста | Да | Да | Базовая функция обеих инструкций |
| Копирование из другого этапа (--from) | Да | Да | Для multi-stage сборки стандарт - COPY --from |
| Автораспаковка локального tar (gzip, bzip2, xz) | Нет | Да | Главная причина непредсказуемости ADD |
| Загрузка файла по HTTP/HTTPS | Нет | Да | Файл получает права 600 по умолчанию, сборка зависит от сети |
| Флаг --chown | Да | Да | Задаёт владельца без отдельного RUN chown |
| Флаг --chmod | Да | Да | Режим доступа задаётся при копировании |
| Проверка целостности загружаемого файла (--checksum) | Нет | Не подтверждено | В доступных источниках флаг не подтверждён; используйте RUN curl с проверкой хеша |
| Предсказуемость результата | Высокая | Ниже | ADD выполняет дополнительные действия неявно |
| Рекомендация по умолчанию | Да | Редкие случаи | Локальный архив под контролем |
Начните с проверки своих Dockerfile: найдите все ADD командой grep -n "^ADD" Dockerfile и замените их на COPY, кроме случаев с локальным архивом. После правок пересоберите образ и сверьте структуру каталогов и владельцев через docker run --rm <image> ls -la, чтобы убедиться, что приложение запускается от непривилегированного пользователя без ошибок доступа.