В компании, где я работаю — большинство сервисов запускаются и работают в docker-контейнерах.


В связи с этим, у моих коллег-новичков-в-докере часто возникает вопрос — а как писать код и запускать его в этом чёртовом контейнере???



Для человека, написавшего около сотни docker-образов и запускающего их несколько раз в день — такой вопрос уже не стоит, но когда я разбирался с докером в давние времена — мысль "Как же писать код в докере? Это же сверхнеудобно!" долго была актуальной.


В статье я опишу свои практики работы с образами docker, которые позволяют писать код "как у себя в home", и даже лучше.


Итак, что такое готовый docker-образ?


Это слепок готового сервиса, который настраивается небольшим числом переменных окружения и готов к работе сразу после старта. С docker-образом не требуется устанавливать зависимости приложения и библиотеки разработчика себе локально в систему, замусоривая её.


Запуск готового образа


Для начала разберём, как запускать образ. Предполагается, что название образа нам известно.


  • название может представлять собой имя образа на hub.docker.com: kaktuss/clickhouse-udp-proxy;
  • название может содержать в себе имя приватного docker registry (репозитория docker образов вашей компании): my-private-registry.com/kaktuss/clickhouse-udp-proxy;
  • в названии может содержаться версия образа: my-private-registry.com/kaktuss/clickhouse-udp-proxy:0.1.

И это всё — имя образа.


В простейшем случае образ запускается так:


docker run --rm -it kaktuss/clickhouse-udp-proxy

Часто образ нужно сконфигурировать переменными окружения. Откуда их брать?


  • нужные значения переменных окружения описаны на docker hub — если это публичный крупный поддерживаемый образ с docker hub;
  • в сценариях nomad, docker swarm, kubernetes, приватной документации — если это приватный образ вашей компании;
  • иногда, переменные нигде не описаны и о нужных значениях требуется догадываться по их названиям и просмотру docker-образа.

Примеры подсмотренных переменных окружения:


С docker hub



Из сценария nomad



Запускаем контейнер с переменными окружения


docker run --rm -it -e CLICKHOUSE_ADDR=127.0.0.1:9000 kaktuss/clickhouse-udp-proxy

Если переменных несколько


docker run --rm -it -e CONSUL_HTTP_ADDR="consul.query.consul:8500" -e VAULT_ADDR="http://vault.query.consul:8200/" -e DC_NAME="deac" -e SYS_NODE="b1" ...

В итоге, мы получили работающий и сконфигурированный сервис.


Попадаем внутрь контейнера


Для упрощения программирования "внутри" контейнера — нам нужно в него попасть. При описанном выше запуске — запускается сам сервис, но мы сами не попадаем "внутрь".


Для попадания "внутрь" — нужно переопределить команду старта образа.


Это делается указанием имени shell-оболочки после имени образа


docker run --rm -it -e CLICKHOUSE_ADDR=127.0.0.1:9000 kaktuss/clickhouse-udp-proxy ash

Shell-оболочка зависит от дистрибутива, на котором построен образ.


  • это может быть ash, как в примере выше;
  • bash;
  • или даже sh, в самом простом случае.

Попробуйте один из вариантов и не ошибётесь.


После подобного запуска мы оказываемся в консоли внутри контейнера.


Инициализация сервиса


После того, как мы попали в консоль внутри контейнера — мы попали в "голый" образ. Часто образы, после запуска, проводят первичную инициализацию (формируют файлы настроек и т.д.) и после этого запускают сам сервис.


Инициализация делается через команду старта, которую мы выше заменили на shell-оболочку. Поэтому, инициализацию нужно запустить вручную. Для этого, открываем Dockerfile и смотрим содержимое инструкции CMD.


CMD ["/usr/local/bin/entrypoint.sh"]

И именно его и запускаем.


/ # /usr/local/bin/entrypoint.sh

или короче


/ # entrypoint.sh

Сервис инициализировался и запустился, теперь мы можем нажать Ctrl+C и снова попасть в консоль, имея контейнер, готовый к повторному запуску сервиса.


Написание кода внутри контейнера


Когда сервис запускается внутри контейнера — он использует те скрипты/бинарные файлы, которые уже находятся внутри. Как нам их редактировать?


Элементарно. Нужно редактировать их извне, в любимом редакторе, в своей домашней папке, а потом просто скопировать в контейнер и запустить.


Даём доступ контейнеру к своей домашней папке используя опцию


-v ~/:/d

docker run --rm -it -e CLICKHOUSE_ADDR=127.0.0.1:9000 -v ~/:/d kaktuss/clickhouse-udp-proxy ash

После запуска данной команды мы будем находиться в консоли контейнера, сможем неограниченно редактировать свой код извне и копировать его в контейнер. Домашняя папка будет доступна в контейнере в папке /d.


/ # cp /d/my-repo/script.pl /usr/local/bin/script.pl

После копирования обновленного скрипта или бинарного файла — запускаем сервис (способом, описанным в разделе инициализации) и получаем сервис с нашими правками, работающий в исходном окружении.


В зависимости от ваших нужд — скрипты/файлы можно не копировать в контейнер, а запускать их сразу из /d/my-repo.


Граничные случаи и лайфхаки


ENTRYPOINT


Некоторые образы (довольно редко) используют команду старта в виде ENTRYPOINT. Что это такое — можно посмотреть в Dockerfile reference. Нам же нужно только помнить, что перезапись команды старта для таких образов выглядит иначе


docker run --rm -it -e CLICKHOUSE_ADDR=127.0.0.1:9000 -v ~/:/d --entrypoint ash kaktuss/clickhouse-udp-proxy

Точка старта переопределяется опцией --entrypoint.


"Облачные" окружения


Если сервисы работают в Consul, docker swarm, kubernetes окружении, то они могут использовать такие переменные окружения, которые доступны только контейнеру, запущенному в этом облаке и не будут доступны контейнеру, запущенному на компьютере разработчика.



В указанном примере используются "облачные" адреса в переменных CONSUL_HTTP_ADDR и VAULT_ADDR. В таких случаях вам нужно использовать внешние адреса данных сервисов.


Повторные запуски


Писать каждый раз полностью команду docker run — излишне. Всю команду старта с переменными удобно сохранить с sh файл. Который потом достаточно просто запускать.


Переиспользование переменных окружения


Если часть переменных окружения требуется для многих сервисов, то эти переменные можно вынести в отдельный файл и передавать контейнеру специальной опцией


docker run --env-file=~/my_docker_env

Запуск без sudo


В локальной разработке запускать контейнеры с sudo — утомительно. Для исправления — добавляем своего пользователя в группу docker. После этого, вместо


sudo docker run ....

можно писать просто


docker run

Комментарии (21)


  1. stagor
    18.01.2018 06:03

    Я бы дописал какую команду запускать в «Запуск без sudo» чтобы знать как именно добавить пользователя в группу. Спасибо.


    1. BuriK666
      18.01.2018 10:32

      sudo usermod -a -G docker username


      1. stagor
        19.01.2018 05:05

        Спасибо :-) Это адресовалось автору статьи, так как команды как запускать без sudo есть, а как добавить пользователя в группу docker — нет. Первое проще, но упомянуто. Второе — сложнее, и нет.


  1. xapon
    18.01.2018 10:44

    Как вы посоветуете решать проблему с установкой библиотек в контейнере, если не хочется поднимать тулчейн языка на хосте? Например, для проекта на nodejs нужен npm install. Если выполнять его в докере, со стандартной структурой проекта с node_modules в корне, то после маппинга volume папка node_modules пропадет из контейнера.


    1. Dreyk
      18.01.2018 11:21

      делать npm install не при билде, а потом, с уже замаунченым volume


      1. xapon
        21.01.2018 04:05

        вариант, но это усложняет запуск (вместо docker-compose up придется выполнять что-то еще)


        1. Dreyk
          21.01.2018 13:44

          можно в entrypoint что-то похожее загнать


          npm check || npm install
          
          exec $@

          небольшой оверхед конечно, но вроде npm сейчас уже быстрый. я так делал с ruby


    1. jaroslavdextems
      18.01.2018 12:58

      сделать следующую структуру:

      /project_cat

      • node_modules/
      • app_sources/
      • package.json


      package.json
        "scripts": {
          "start": "node server.js"
        },


    1. DAiMor
      18.01.2018 13:00

      у меня на angular5 приложение. node_modules нужно в отдельный volume
      Dockerfile

      FROM node:alpine
      RUN apk update && apk add git
      WORKDIR /opt/app
      COPY package.json .
      RUN npm install
      ENV PATH="$PATH:./node_modules/.bin"
      VOLUME /opt/app/node_modules
      CMD npm run serve:docker

      собираем и запускаем
      docker create -t myapp .
      docker volume create node_modules
      docker run -v node_modules:/opt/app/node_modules -v `pwd`:/opt/app -p 4200:4200 myapp

      я вообще docker-compose использую, чтобы еще бекенды запускались


    1. pina
      18.01.2018 13:54

      В docker-compose

      volumes:
            - ./backend:/app
            - /app/node_modules


      Первый элемент — забрасывает локальную папку(./backend) в контейнер по пути(/app)
      Второй элемент — это трюк который дает ожидаемое поведение, подробнее в статье jdlm.info/articles/2016/03/06/lessons-building-node-app-docker.html


      1. xapon
        21.01.2018 03:57

        Да, тоже видел эту статью.
        Работает — но с существенным ограничением: на хосте папка node_modules не появляется. А это значит отсутствие автодополнения и возможности модификации библиотек с хоста.
        Конечно, можно потом эту папку скопировать через docker cp, но это во-первых муторно и печально, а во-вторых, изменения файлов все равно не синхронизируются.


  1. oas
    18.01.2018 12:37

    Docker же мёртв? :)


    1. AstarothAst
      18.01.2018 14:04

      А он в курсе?


    1. neenik Автор
      21.01.2018 14:16

      Мертв-не мёртв, но работать иногда отказывается :)


      Скрытый текст


  1. Valtasaar
    18.01.2018 20:04

    Знает кто статью, в которой описан запуск и работа с приложением на RubyOnRails через докер в винде? То что я находил там совсем непонятно.


    1. voe
      21.01.2018 10:01

      На вине можно и без докера запускать через поддержку линукса оно даже работает как нужно. Если не нужно запускать GUI приложения то проблем нету особых.


      1. Valtasaar
        21.01.2018 12:47

        Проблем тоже хватает. Самая главная это скорость работы. Запуск вэб-приложения в несколько раз медленнее.


  1. voe
    21.01.2018 10:09

    Статья не отражает суть контент изложенного в ней, по названию написание кода в докер контейнере, а по факту мы его там только выполняем. Для написание все равно придётся ставить ide и к ней наверняка понадобятся ещё и библиотеки, меньше чем к языку программирования но все равно понадобятся, а если нужна будет консоль от языка программирования? То что делать? Бежать в докер контейнер и забить на поддержку консоли в ide?


    1. neenik Автор
      21.01.2018 14:12

      Всегда можно взять в руки vim и идти править файлы внутри запущенного контейнера. Цель статьи была показать, что это неудобно и править файлы удобнее вне контейнера и сразу же их запускать/тестировать в настроенном окружении.
      Если же нужна консоль контейнера — то одна открытая (на момент запуска контейнера) у вас уже есть. Если же нужны дополнительные, то "docker exec -it bash" в том числе и в консоли ide.


  1. missing_thing
    21.01.2018 13:50
    +1

    Если вы запускаете контейнер на хосте под управлением Linux, при монтировании домашней директории хорошо бы сделать mapping текущего пользователя, иначе файлы созданные из контейнера будут принадлежать пользователю контейнера (чаще всего это root). Docker for Mac лишен этого недостатка и делает mapping автоматически.


    1. neenik Автор
      21.01.2018 14:07

      Справедливое замечание. Сам так делаю редко, но если в результате работы нужны файлы, сгенерированные контейнером — то да.