Замена npm-ссылок в Docker-контейнере (случай для образа “node:12.4” и файла“docker-compose”)
После долгих изысканий о том, как лучше организовать инкрементальную сборку проекта в условиях локальной разработки для проекта с Docker-ом, я выбрал такое концептуальное решение:
Наличие Docker-а никак не должно влиять на работу инструментов для сборки проекта (gulp, webpack и т. д.), в частности навязывать новую файловую структуру проекта.
Я думаю, ввиду мнения специалиста @GrayHoax, такая концепция имеет право на существование:
Приложение, завёрнутое в докер ничем не отличается от приложения, выложенного на обычную виртуалку (за исключением способа развертывания).
Проект
В проекте имеется несколько зависимостей (допустим dependency_a и dependency_b), доступ которым осуществляется с помощью инструмента Lerna. Для тех, кто с ним не знаком, скажу что судя по всему этот инструмент использует обычные npm-сысылки, а сам лишь только автоматизирует процесс их добавления.
Как бы Вы не критиковали структуру моего проекта за её непохожесть на обычный src/dest-шаблон, без Docker-а сборка проекта работает и собранное full-stack приложение помещается в папку 02-DevelopmentBuild (для режима локальной разработки).
Теперь нужно поместить этот проект в Docker-контейнер без прямого копирования, причем пути Docker должен правильно понять ссылки dependency_a и dependency_b.
Теперь, со следующим docker-compose.yaml:
version: "3"
services:
logic:
image: node:12.4
command: npm run 'SPA incremental building' && npm run 'Run server'
volumes:
- .:/Application
working_dir: /Application
Кстати, именно этот способ обеспечения доступа к локальным файлом мне порекомендовали в ответе на вопрос Как правильно объявлять Volum-ы в длинном синтаксисе Docker-а?. Увы, этот способ не учитывает npm-ссылки, потому возникает ошибка:
Error: Cannot find module '/packages/dependency_a/bin/dependency_a'.
Директория packages находится на уровень выше проекта, но возможно это для Lerna это не столько директория, сколько алиас.
На всякий случай покажу, как это выглядит в косоли:
Другими словами, Docker не видит npm-ссылку, актуальную для файловой системы локального компьютера.
Концепция решения
С точки зрения NPM и Lerna у меня всё сделано правильно ввиду того, что без Docker-а сборка проекта работает. Но это "правильно" для Docker-контейнера неактуально.
Docker не должен навязывать условий, соблюдение которых поломает обычную сборку проекта без Docker-а (это что будет если каждый инструмент, будь то Docker, Gulp или Webpack начнут навязывать свои структуры проекта и так далее).
Таким образом, Docker должен предоставлять функционал (API), который позволит объяснить только ему, где же надо искать
dependency_aи другие.
Попытки решения без понимания
Ввиду того, что я задаю этот вопрос, я не знаю, как надо делать правильно, потому я эксперимента ради попробовал такое решение:
version: "3"
services:
logic:
image: node:12.4
command: npm run 'SPA incremental building' && npm run 'Run server'
volumes:
- .:/Application
- ../../packages/dependency_a:/Application/node_modules/dependency_a
- ../../packages/dependency_b:/Application/node_modules/dependency_a
working_dir: /Application
# ports:
# - "3000:3000"
Естественно, оно нерабочее. Вдобавок к этому, я не совсем понял, что сделал. Наконец, я и сам чувствую, что создание пересекающихся bind-ов (или создание одного поверх другого) - это что-то не то.
Сообщение об ошибке то же, что и раньше.
Анализ статьи о подобной проблеме
Подобная ситуация рассматривается в статье Developing a new Node module in a Docker container without using NPM link. Вот какое решение она предлагает:
RUN mkdir -p /usr/src/node_modules
ENV PATH /usr/src/node_modules/.bin:$PATH
docker-compose.yaml:
volumes:
- .:/usr/src/app
- ../redux-beacon-slack:/usr/src/node_modules/redux-beacon-slack
Оно мне не подходит в первую очередь потому, что у меня образ другой, не операционной системы, а node:12.4.
Недопустимые решения
Использовать только опубликованные npm библиотеки
Удобно организованная среда разработки является базовым требованием современной IT-индустрии. Применительно к npm это значит:
- Не должно быть никаких лишних релизов
npm-библиотек ("чтобы просто посмотреть отладочную информацию"). - Если мы внесли какое-либо изменение в локально разрабатываемую npm-зависимость, то мы должны в реальном времени получать обратную связь от этого. Например, добавили
console.log()в зависимость - тут же видим вывод в консоль при запуске основного приложения.
Изменение структуры проекта
ИМХО хороший инструмент должен адаптироваться под структуру уже существующих проектов, а не навязывать свою собственную.
Насчет Dockerfile
Как видите, его в проекте нет. Увидев пример без Docker файла, я решил убрать Dockerfile и добавить его снова только если на то будет рациональная причина.
Часто задаваемый вопрос: зачем мне Docker?
Нужен он мне собственно затем, зачем его и разрабатывали:
- Не зависеть от версии локально установленного NodeJS.
- Избежать полностью ручной установки NodeJS и других инструментов (например, баз данных) при деплое на продакшен.
В данном приложении заменить локальный Node.JS на Docker-образ - это лишь первый шаг, далее я буду добавлять образ с базой данных.
О копировании файлов
На англоязычном StackOverflow первый конкурс на английскую версию этого вопроса закончился без ответа. В комментариях люди что-то постоянно говорили про копирование файлов.
Этот момент я прояснил в вопросах Есть ли в Docker-e концепция доступа к локальным файлам без их прямого копирования? b Как правильно объявлять Volum-ы в длинном синтаксисе Docker-а? и вот какой вердикт: в режиме локальной разработки ничего копировать не надо, потому что такой принцип нарушит инкрементальную сборку, то есть автоматическую пересборку проекта в реальном времени при внесении изменений в исходный код и автоматическую перезагрузку страницы браузера при обновлении сборки. Вместо этого, нужно обеспечить доступ к проекту из Docker-контейнера (это уже достигнуто обычным bind-ом) и теперь объяснить ему, как интерпретировать неактуальные для него npm-сыылки.
Ответы (1 шт):
Приложение, завёрнутое в докер ничем не отличается от приложения, выложенного на обычную виртуалку
Это справедливо с оговорками и чуть ниже о них напишу, а сейчас примем это как исходный посыл. Проблема, которая у вас возникает, а именно
Docker не видит npm-ссылку, актуальную для файловой системы локального компьютера
возникает потому, что вы не трактуете приложение в докере таким же образом, как бы вы его запускали на виртуалке в той части, что вы запускаете npm install или его аналог на хосте. Из-за этого возникают ссылки с путями на локальном хосте. Если вы проделаете такую же операцию с виртуальной машиной, т.е. установите зависимости на локальном хосте, а потом папку с приложением скопируете или примонтируете в файловую систему виртуалки, то получите ровно ту же проблему, что имеете сейчас в докере. Виртуалка или докер контейнер не знают ничего о файловой системе вашего хоста, и не могут обращаться к файлам в файловой системе хоста.
В этом вопросе описан способ, как сделать инкрементную сборку. И важный момент там заключается в том, что инсталяция пакетов происходит внутри контейнера. Именно это является главной проблемой у вас сейчас.
Обратите внимание также, что в этом подходе все манипуляции с зависимостями при разработке с использованием докера (например добавление нового пакета) происходит в самом контенере командами типа:
docker-compose run buildchain yarn add cat-names --dev
В вашем случае нужно запускать соответствующие команды Lerna, но тоже внутри контейнера.
Конечно, для ускорения работы имеет смысл сконфигурировать контейнер так, чтоб node_modules сохранялся между запусками (т.е. использовать volume). Это все есть той статье.
Теперь что касается параллелей между виртуалкой и докером. Это правда, что нет разницы, где приложение запускается в виртуалке или в докере. Но есть большая разница используется инкрементнальная сборка или нет. В prod инсталяциях создаются неизменяемые образы. Даже если нужно внести какое-то изменение на виртуалку, то правильный подход - это пересоздать ее (возможно на основе какого-то подготовленного образа). Виртуалки а тем более докер контейнеры должны быть immutable, в чем и заключается суть разделения на сборку и запуск из 12 factor app. Запуск в инкрементальном режиме - это уже отход от того, чтобы запускать приложение в том же режиме, что и на проде. Тем более, когда у вас есть необходимость синхронизации кода между двумя хостами. И нормально и ожидаемо, что это будет влиять на способ развертывания и запуска. Вопрос только в том, чтобы сделать это по возможности безболезненно и на какие компромиссы прийдется пойти.

