О том, зачем нужен Docker — написано сто тысяч статей, и примерно столько же докладов. Но у меня возникла вполне реальная проблема, под которую Docker ложился идеально. Совсем недавно я начал один небольшой проект на Node.js. Сначала все шло легко и просто, но внезапно я понял, что понятия не имею, где в open source проектах хранят секретные данные. Ну, то есть, обычно всякие пароли и секретные ключи я храню в конфигурационных файлах, ведь репозитории приватные. А тут GitHub, открытый проект. После недолгого гугления оказалось, что реальные пацаны давно хранят секретные ключи в переменных окружения. Задавать пару десятков переменных окружения каждый раз вручную… Тут-то на ум и пришел Docker.
Хотя про Docker я слышал уже много, но попробовать его на чем-то реальном пока не получалось. И вот настал тот час. Сразу оговорюсь, что в статье я описываю настройку докера для локальной разработки, для продакшна такая конфигурация никак не пойдет.
Раз я строю контейнер для локальной разработки, ничего сложного мне не нужно. Мне нужен контейнер, в котором будет крутиться только один Node.js процесс. Исходник проекта я хочу подключать как внешний том из моей системы и монтировать его в директорию /var/app. При изменении исходников в моей системе Node.js процесс должен перезапускаться. Порт 3000 должен пробрасываться из контейнера в мою систему, чтобы я мог видеть свой проект в браузере. Вроде ничего сложного.
Dockerfile
Создадим Dockerfile, который описывает нужную мне структуру образа. Называться он должен ‘Dockerfile’ и никак иначе. Вот содержимое моего:
1 2 3 4 5 6 7 8 9 10 |
FROM ubuntu:latest MAINTAINER Vladimir Boliev "voff.web@gmail.com" RUN apt-get update -y --no-install-recommends RUN apt-get install -y nodejs nodejs-legacy npm RUN mkdir /var/app RUN npm install -g --yes pm2 WORKDIR "/var/app" ADD . "/var/app" CMD npm install CMD pm2 start --no-daemon --watch --name="thecoderstack" npm -- start |
Разберем построчно.
1 |
FROM ubuntu:latest |
Объявляю образ на основе последней версии Убунты. Вообще, это не самый лучший выбор. В итоге, мой образ на основе убунты занимает 485 мб. Такой же образ на основе Alpine, будет занимать в десятки раз меньше места. Но, так как я в докере новичок, хотелось иметь под рукой хорошо знакомую ось.
2 |
MAINTAINER Vladimir Boliev "voff.web@gmail.com" |
Просто мои координаты
3 |
RUN apt-get update -y --no-install-recommends |
Обновляю индекс пакетов
4 |
RUN apt-get install -y nodejs nodejs-legacy npm |
Устанавливаю nodejs и npm
5 |
RUN mkdir /var/app |
Создаю директорию, в которой будет находиться проект
6 |
RUN npm install -g --yes pm2 |
Устанавливаю pm2. Вообще-то локально я обычно использую nodemon, но у него есть проблемы с докером: при несовпадении временной зоны на локальной машине и в контейнере он перестает понимать, что в файле произошли изменения. Если вам критично использовать именно nodemon, можете почитать это issue.
7 |
WORKDIR "/var/app" |
Устанавливаю директорию /var/app как рабочую. Все дальнейшие команды будут выполняться из этой директории.
8 |
ADD . "/var/app" |
“Точка” — указывает на рабочую директорию
9 |
CMD npm install |
Запускаю npm install в рабочей директории
10 |
CMD pm2 start --no-daemon --watch --name="thecoderstack" npm -- start |
Запускаю pm2. Обратите внимание, что запускать нужно с параметром —no-daemon, иначе докер решит, что процесс закончил работу и остановит контейнер.
Строю образ
Чтобы из этого файла построить образ, нужно в директории с Dockerfile’ом выполнить команду:
1 |
$ docker build -t boliev/thecoderstack:local . |
Параметр -t задает имя образа в формате namespce/name:tag.
Namespace можно не задавать, но оно понадобится, если потом захотите запушить образ в докер хаб. Да и вообще так нагляднее. Имя может быть любое, в данном случае это название моего проекта. Tag… это тег. Это может быть версия или название окружения.
Обратите внимание на точку, завершающую команду. Точка означает, что искать Dockerfile нужно в текущей директории.
После выполнения команды, образ построится и будет виден в системе:
1 |
$ docker images |
Образы-контейнеры
Все это время я строил образ, хотя вроде запускать мне нужно контейнер. Важно понимать разницу между этими понятиями. Образ для каждого контейнера может быть только один, а контейнеров, запущенных на основе одного образа, может быть сколько угодно.
В терминах ООП образ — это класс, а контейнер — это объект. Причем образы даже могут наследоваться друг от друга. Первая строчка FROM как раз означает, что мой образ “наследуется” от образа последней убунты. Именно поэтому на мой компьютер скачался образ убунты, когда я выполнил docker build.
Когда я строил Dockerfile, то некоторые команды у меня начинались с RUN, а некоторые с CMD. Разница в том, что RUN выполняется на этапе сборки образа, а CMD уже после создания контейнера. Например, инструкция:
1 |
CMD npm install |
не может выполниться на этапе сборки образа, ведь исходные файлы проекта подключаются только когда мы создаем контейнер. Кстати об этом, пора создать наш контейнер.
Поднимаем контейнер
1 |
docker run -tid -p 127.0.0.1:3000:3000 -v /home/voff/work/thecoderstack:/var/app boliev/thecoderstack:local |
- -ti: Создает псевдотерминал и дает возможность подключаться и отключаться от докер контейнера;
- -d: Запускает контейнер в режиме демона, в бэкграунде;
- -p: Пробрасывает 3000 порт контейнера на 3000 порт моей рабочей системы;
- -v: Монтирует директорию /home/voff/work/thecoderstack рабочей системы в директорию /var/app в контейнере;
- boliev/thecoderstack:local: имя docker образа.
Если все прошло успешно, вам покажут идентификатор контейнера, а в браузере по адресу http://localhost:3000/ станет доступен ваш проект.
Теперь все работает, но мне бы еще хотелось смотреть вывод Node.js консоли.
Чтобы посмотреть список запущенных контейнеров, выполните команду:
1 |
$ docker ps |
Теперь можно “войти” в контейнер указав его ID
1 |
$ docker attach aaa5bd8af22b |
Вернитесь в браузер, обновите страницу с проектом, и в консоль выведутся логи.
Чтобы выйти из контейнера не останавливая его, нажмите последовательно комбинации ctrl+p ctrl+q.
В итоге
Приручил ли я Docker? Нет, конечно нет. Docker — это такая штука, которую нельзя изучить по документации или по статьям. Нужно с ним работать, нужно его использовать, нужно подчинять его себе день за днем. Проблема также в том, что каждый его использует по своему. Нет единого мнения, стоит ли подключать код как внешний том, или делать git clone в Dockerfile. Нужен ли вообще attach? Должны ли логи быть внешним, общим для всех контейнеров, томом? Можно ли запускать в докере больше одного процесса? Вопросов по докеру — море, и правильные ответы подскажет только постоянная практика.