Кеш сборки и lockfile
API уже запускается из image. Теперь рассмотрим повторную сборку после правки server.mjs. В простом Dockerfile исходники копируются раньше npm ci, поэтому изменение одного серверного файла затрагивает входы следующего шага установки. В этом уроке перестроим порядок инструкций и научимся читать кеш по зависимостям шагов, а не по обещанию постоянной скорости.
Наш API не имеет внешних npm-пакетов. Это удобная граница для изучения: не будем приписывать пустой установке большой выигрыш времени. В будущем тот же порядок пригодится клиенту Vite, у которого реальные зависимости есть. Результат урока — полный обновлённый Dockerfile и два ожидаемых сценария изменения входных файлов.
Выделим манифесты раньше исходников
Замените api/Dockerfile целиком следующим содержимым. Остальные файлы из предыдущего состояния сохраняются, включая .dockerignore.
FROM node:24-bookworm-slim
WORKDIR /app
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
USER node
EXPOSE 4100
CMD ["node", "src/server.mjs"]
Первая инструкция COPY переносит только package.json и package-lock.json. После неё npm ci получает необходимые входы, но ещё не видит исходники сервера. Вторая COPY добавляет остальные разрешённые файлы контекста. Изменение server.mjs теперь относится к более поздней части последовательности и не обязано нарушать возможность повторного использования раннего результата установки.
Такая перестановка не означает, что Docker знает смысл каждой строки JavaScript. Builder рассматривает инструкции и их входные материалы. Мы сами выразили полезную границу: описание пакета меняется реже, чем реализация HTTP-маршрута. Правило порядка шагов и другие способы работы с кешем описаны в документации Docker о кешировании сборки.
COPY . . во второй части снова включает манифесты, но это не делает исходники входом уже завершённого раннего COPY. Если меняются package.json или lockfile, ранний шаг действительно должен измениться. Если меняется только server.mjs, установка имеет прежние входы. Разделение становится понятным, когда читаем Dockerfile сверху вниз по конкретным данным.
Lockfile нужен для договора установки
npm ci требует существующий package-lock.json или подходящий npm-shrinkwrap и согласованность с package.json. Команда не предназначена для незаметного исправления несовпавшего договора зависимостей. Она также отличается от обычной интерактивной установки, при которой разработчик намеренно изменяет дерево пакетов. Назначение команды описано в официальной документации npm ci.
В нашем API полный lockfile подготовлен для собственного пакета без внешних зависимостей. Это не сокращённый lockfile Vite и не шаблон, куда можно вручную вписать выдуманные integrity. Когда клиент получит реальные пакеты, читатель сначала разрешит их настоящим package manager, сохранит полученный файл и только затем использует npm ci в image.
Несогласованные манифесты нельзя исправлять заменой команды на npm install внутри Dockerfile, не объяснив изменение требований. Такая подмена делает результат сборки зависимым от нового разрешения пакетов. Она может скрыть первопричину ошибки, но не сохранить предсказуемый договор. Здесь требуем согласованный вход и ожидаем явный отказ при его нарушении.
Сопоставим изменения со сборкой
| Изменение | Какая граница затронута | Что ожидаем при доступном кеше |
|---|---|---|
| Только server.mjs | Позднее копирование исходников | Ранний шаг npm ci может быть использован повторно |
| package-lock.json | Ранний вход установки | npm ci должен получить новые входы |
| Базовый образ | Начало последовательности | Старые результаты могут перестать подходить |
| Исключённый журнал | Не входит в обычный context | Не должен менять COPY из-за содержимого журнала |
| Новый builder без кеша | Место хранения результатов | Шаги могут выполниться заново |
Наличие прежних входов — условие возможности повторного использования, а не обещание конкретного слова CACHED в любой среде. Кеш может быть удалён, builder может быть другим, базовый image может обновиться. Даже при повторении проекта на другой машине локальные результаты предыдущей сборки не появляются там автоматически.
Проведём ожидаемое сравнение
Читатель сначала собирает состояние без дополнительных изменений. Для наглядного журнала можно запросить обычный текстовый вывод:
docker build --progress=plain --tag professorweb-container-lab-api:lab-07 ./api
После успешной самостоятельной сборки измените только сообщение запуска в server.mjs, например добавив слово lab к диагностической строке. Это точное упражнение с исходником, которое не меняет договор ответа /api/courses. Повторите ту же команду и найдите шаги манифестов, npm ci и позднего копирования.
Ожидается, что изменение исходника будет видно в позднем COPY. Ранние шаги при сохранённом кеше могут быть отмечены повторно использованными. Не сравнивайте секунды с заранее заданным числом: здесь не проводились измерения, а скорость зависит от доступного кеша, машины и сетевых условий. Полезный результат — объяснение причины, по которой один шаг зависит от изменения, а другой не зависит.
Затем верните исходное сообщение, чтобы продолжить серию с полным after из архива. Если хотите исследовать изменение манифестов, делайте это в отдельной копии и с настоящим package manager. Не добавляйте случайную внешнюю зависимость только ради кеша: она расширит задачу до сети registry и нового lockfile, что пока не требуется.
Не спутайте кеш и обновление процесса
Даже успешная новая сборка не меняет уже существующий pw-catalog-api. Он создан из прежнего image. Чтобы увидеть изменённое сообщение, нужен новый экземпляр соответствующей сборки; простой start старого контейнера продолжает его прежнее состояние. Тег позволяет найти новый image, однако связь существующего контейнера с выбранным image при создании не становится динамической ссылкой.
Если на повторной сборке неожиданно повторилась установка, проверьте не только server.mjs. Возможно, вместе с ним изменился lockfile, вариант базового образа или builder. Широкое копирование до установки снова создаст ту проблему, которую мы только что устранили. Для диагностики важен фактический порядок Dockerfile, а не фраза «у меня стоит Docker cache».
Кеш сборки также не равен named volume с каталогом. Первый хранит результаты операций builder, второй позже сохранит данные приложения. Удаление одного ресурса не объясняет изменение другого. В проекте с frontend появится ещё кеш package manager и отдельная папка node_modules; каждой сущности нужно своё имя и причина существования.
Зафиксируем границу оптимизации
Полные start/after находятся в архиве серии. После урока меняется только Dockerfile: API, схема каталога и маршруты остаются прежними. Execution в STATE — not_run; текст описывает ожидаемое сравнение, а не выполненный benchmark.
Порядок инструкций сделал зависимости сборки более точными. В следующем уроке разберём теги и удаление ресурсов, чтобы сохранённый кеш или старый image не путался с текущим работающим экземпляром. Затем перейдём к mounts, где исходные файлы смогут подключаться на этапе запуска и вообще не требовать новой сборки при каждом изменении.