Текст и данные · Инструкция
pnpm fetch в Docker: как ускорить сборку
pnpm fetch в Docker позволяет заранее загрузить зависимости в virtual store, опираясь в основном на lockfile, а.
Короткий ответ
Скопируйте `pnpm-lock.yaml` и workspace-конфигурацию до исходников, выполните `pnpm fetch`, затем скопируйте проект и запустите `pnpm install -r --offline`. Такой порядок особенно полезен в monorepo, потому что не требует по одному копировать все `package.json` только ради cache-слоя.
Зачем существует pnpm fetch
Обычный Dockerfile часто копирует `package.json` и lockfile, запускает install, а потом копирует исходники. В одном приложении это достаточно просто, но в monorepo список manifests растёт: новый workspace-пакет требует менять Dockerfile, иначе слой зависимостей перестаёт отражать структуру репозитория. `pnpm fetch` решает именно эту проблему. По официальной документации команда загружает пакеты из lockfile в virtual store и игнорирует package manifest для определения обычных зависимостей; поэтому ранний слой можно построить вокруг lockfile и workspace-настроек. Когда исходный код меняется без изменения lockfile, слой fetch остаётся кешируемым Docker. После копирования проекта выполняется offline install, который связывает уже загруженные пакеты с workspace. Это не «ускоритель сети» сам по себе: выигрыш появляется тогда, когда Docker действительно может переиспользовать неизменившийся слой между сборками.
Важно: Порядок `COPY` важнее самой команды. Если скопировать весь репозиторий до `pnpm fetch`, любое изменение исходника снова инвалидирует слой и основное преимущество исчезнет.
Базовая схема Dockerfile
- Подготовьте pnpm в build stage: Используйте принятый в проекте способ установки/активации pnpm и фиксируйте его версию через инфраструктуру проекта, чтобы сборка была воспроизводимой.
- Скопируйте lockfile и workspace config: До исходников скопируйте `pnpm-lock.yaml`, `pnpm-workspace.yaml` и файлы конфигурации, которые влияют на resolution. Если используются pnpm patches, их тоже нужно скопировать до fetch.
- Выполните `pnpm fetch`: Команда загрузит данные из lockfile в store. Если stage предназначен только для production, официальная CLI поддерживает `pnpm fetch --prod`.
- Скопируйте исходный код: Только после fetch перенесите package manifests и остальные файлы приложения или workspace.
- Выполните offline install: Используйте `pnpm install -r --offline` с нужными production-флагами. Он должен работать из данных, уже помещённых в store.
- Соберите и сформируйте runtime stage: Запустите build, затем перенесите в финальный образ только то, что нужно приложению в runtime, согласно архитектуре проекта.
Совет: Если у вас используются `pnpm.patchedDependencies`, каталог с patch-файлами должен попасть в контекст до `pnpm fetch`; официальная документация отдельно отмечает это требование.
Почему cache становится стабильнее
Docker определяет возможность повторного использования слоя по предыдущим инструкциям и входным файлам. Если install-layer зависит от десятков `package.json`, изменение версии скрипта, metadata или добавление нового workspace легко сбрасывает cache. `pnpm fetch` делает зависимость слоя ближе к фактическому resolution: основным входом становится lockfile. Это особенно полезно, когда в pull request часто меняется код, но набор зависимостей остаётся прежним. Однако не следует скрывать от слоя файлы, которые действительно влияют на resolution: workspace config, pnpm config и patches должны участвовать в cache-key через `COPY`. Иначе сборка может использовать слой, созданный для другой конфигурации. Для проверки эффекта сравните две последовательные Docker-сборки: во второй измените только исходный `.ts` или `.js` файл. Слой `pnpm fetch` должен быть взят из cache, а build-слои после `COPY .` — пересобраться.
Monorepo, production и file: зависимости
В monorepo `pnpm fetch` полезен тем, что ранний слой не надо вручную синхронизировать со списком workspace manifests. После копирования репозитория recursive offline install получает полную структуру workspace и создаёт нужные связи. Если вы строите production-образ, можно отделить production dependencies с `pnpm fetch --prod`, но конкретная схема build/runtime зависит от того, нужны ли devDependencies для компиляции. Нередко удобнее иметь build stage с полным набором зависимостей, а затем отдельный runtime stage с production-only содержимым. В документации есть важное ограничение: зависимости с протоколом `file:` во время fetch пропускаются, поскольку локальные файлы ещё могут отсутствовать; они обрабатываются на install-этапе после копирования workspace. Не пытайтесь делать `--offline` до того, как все требуемые внешние пакеты оказались в store. Если offline install обращается в сеть, это сигнал, что fetch-layer неполон или конфигурация между этапами различается.
Что чаще всего ломает оптимизацию
Первая ошибка — `COPY .` перед `pnpm fetch`: cache становится чувствительным ко всему исходному дереву. Вторая — забытый patch-файл или config, влияющий на resolution; тогда ранний слой не соответствует реальной установке. Третья — предположение, что `fetch` заменяет `install`: команда лишь заполняет virtual store, но не строит окончательное `node_modules` для workspace. Четвёртая — смешивание разных версий pnpm между fetch и install stage. Пятая — попытка измерять успех только по размеру образа: `fetch` прежде всего оптимизирует повторное использование слоёв и время rebuild, а финальный размер определяется multi-stage архитектурой и тем, что вы копируете в runtime. Наконец, следите за `.dockerignore`: лишние файлы увеличивают контекст и могут инвалидировать более поздние слои, даже если dependency cache организован правильно.
Предупреждение: Не копируйте `.npmrc` с постоянным registry-токеном в финальный образ. Если приватный registry требует секрета на build-этапе, используйте безопасный механизм build secrets вашей инфраструктуры и не запекайте credential в layer.
Как доказать, что dependency-layer действительно кешируется
После изменения Dockerfile измерьте результат двумя контролируемыми сборками. Сначала соберите образ с чистым cache и сохраните лог шагов. Затем измените только файл исходного кода, который не влияет на `pnpm-lock.yaml`, workspace config или patches, и запустите сборку снова. Шаг `pnpm fetch` должен быть взят из Docker cache, тогда как инструкции после `COPY .` могут пересобраться. Третий полезный тест — изменить сам lockfile; в этом случае fetch-layer обязан инвалидироваться. Такая тройка проверок быстро показывает, привязан ли cache-key к правильным файлам. Не сравнивайте только общее время сборки: параллельная сеть, прогрев registry и build cache могут скрывать эффект. Смотрите на конкретный статус слоя в build-логе. Если `pnpm fetch` пересобирается после любого изменения `.ts`/`.js`, проверьте порядок `COPY`; если не пересобирается после изменения lockfile, значит Dockerfile копирует не тот файл или использует неожиданную build context.
Проверка Dockerfile
- `pnpm-lock.yaml` копируется до исходников
- Workspace config и patches копируются до fetch
- `pnpm fetch` выполняется до `COPY .`
- После source используется offline install
- Версия pnpm одинакова на связанных этапах
- Секреты registry не попадают в финальные слои
- Повторная сборка с изменением только source переиспользует fetch-layer
Что учитывать
Docker определяет возможность повторного использования слоя по предыдущим инструкциям и входным файлам. Если install-layer зависит от десятков `package.json`, изменение версии скрипта, metadata или добавление нового workspace легко сбрасывает cache. `pnpm fetch` делает зависимость слоя ближе к фактическому resolution: основным входом становится lockfile. Это особенно полезно, когда в pull request часто меняется код, но набор зависимостей остаётся прежним. Однако не следует скрывать от слоя…
Источники и проверка
Фактическая часть сверена по первичным источникам (в т.ч. pnpm fetch). Пример и формулировки — редакция N1RO на 2026-09-21.