Глава 10. Некое незначительное брюзжание

Ну, это не должно вас смущать.

К сожалению, у Compose имеется пара смущающих проблем. Поскольку вы, вероятно, уже сталкивались с ними в предыдущих главах, либо по мере того как применяли Compose самостоятельно, будет безответственным проигнорировать их. Здесь мы рассмотрим каждую из них по очереди.

К счастью, мы можем обойти первую проблему без особых усилий. Тем не менее, вторая остаётся неразрешённой.

Rails tmp/pids/server.pid не очищается

По неким причинам, порой случается так,что в процессе прекращения определённого приложения Compose (нажатием Ctrl-C), ваш сервер Rails, похоже, не отключается аккуратно и его файл server.pid - который хранится в tmp/pids/ - не удаляется. Это означает, что когда вы запустите это приложение снова при помощи:


​ 	​$ ​​docker-compose​​ ​​up​
		

вы можете обнаружить себя в противостоянии со следующей ошибкой в своём выводе:


​ 	​...​
​ 	A server is already running. Check /usr/src/app/tmp/pids/server.pid
​ 	​...​
		

Наличие данного файла pid заставляет запускаемый сервер предполагать, что уже имеется некий запущенный сервер, поэтому он не запустится.

Rails сохраняет такой файл server.pid в tmp/pids. Так как мы монтируем свой локальный каталог приложения в самом контейнере, этот файл попадает в соответствующий каталог tmp/pids/ в нашей локальной машине и присутствует там пока мы не удалим его.

Как решить эту проблему?

Так как мы монтируем свой каталог приложения в запускаемом контейнере, достаточно просто удалить этот файл server.pid вручную:


​ 	​$ ​​rm​​ ​​tmp/pids/server.pid​
		

После выполнения этого наш сервер Rails теперь должен запускаться:


​ 	​$ ​​docker-compose​​ ​​up​
		

В своём выводе вы должны обнаружить что Rails теперь поднят и запущен. Тем не менее, это не является в действительности решением проблемы, если оно продолжает происходить. К счастью, мы можем найти обходной путь.

Давайте рассмотрим этот обходной путь, а потом обсудим его:

  1. Созлайе в своём корне Rails файл docker-entrypoint.sh со следующим содержимым:

    
    ​ 	​#!/bin/sh​
    ​ 	set -e 
    ​ 	
    ​ 	​if​ [ -f tmp/pids/server.pid ]; ​then​ 
    ​ 	  rm tmp/pids/server.pid            
    ​ 	​fi​
    ​ 	
    ​ 	exec ​"​$@​"​
     	   
  2. Сделайте этот файл исполняемым:

    
    ​ 	​$ ​​chmod​​ ​​+x​​ ​​docker-entrypoint.sh​
     	   
  3. В нашем файле Dockerfile определите инструкцию ENTRYPOINT добавив следующую строку прямо перед самой последней инструкцией CMD:

    
    ​ 	ENTRYPOINT [​"./docker-entrypoint.sh"​]
     	   
  4. Остановите, соберите заново и перезапустите свою службу web:

    
    ​ 	​$ ​​docker-compose​​ ​​stop​​ ​​web​
    ​ 	​$ ​​docker-compose​​ ​​build​​ ​​web​
    ​ 	​$ ​​docker-compose​​ ​​up​​ ​​-d​​ ​​web​
     	   

Итак, что это всё означает?

Некая entrypoint присоединяется спереди к нашей команде запуска при старте некого контейнера. В нашем случае мы установили ./docker-entrypoint.sh в качестве такой ENTRYPOINT для нашей службы web. Это означает, что когда мы запускаем некий новый контейнер web, вместо того чтобы просто запускать нашу команду по умолчанию:


​ 	bin/rails s -b 0.0.0.0
 	   

мы в действительности выполним это благодаря своей новой инструкции ENTRYPOINT:


 	./docker-entrypoint.sh bin/rails s -b 0.0.0.0
 	   

Так как этот сценарий оболочки будет запускаться, нам приходится давать его файлу полномочия execute, что мы и сделали на шаге 2.

Что в действительности делает наш сценарий docker-entrypoint.sh? На тот сдучай если вы не знакомы с Bash, давайте быстренько пробежимся по нему шаг за шагом (не стесняйтесь перейти далее, если вам всё понятно).


​1: 	​#!/bin/sh​
​2: 	set -e 
​3: 	
​4: 	​if​ [ -f tmp/pids/server.pid ]; ​then​ 
​5: 	  rm tmp/pids/server.pid            
​6: 	​fi​
​7: 	
​8: 	exec ​"​$@​"​
 	   

Сценарии Bash рекомендуется начинать с set -e (строка 2) - это приводит к быстрому отказу самого сценария в случае завершения последующих команд с некой ошибкой (не нулевым состоянием выхода).

Оператор if в строке 4 выполняет проверку на наличие файла tmp/pids/server.pid; если это так, мы удаляем его в строке 5. Это порция очистки в данном сценарии гарантирует что наш сервер всегда запускается, даже если был оставлен нетронутым файл server.pid.

Однако в конечном счёте мы желаем чтобы этот контейнер запускал наш сервер Rails не в данном сценарии Bash. Именно здесь и приходит на помощь соответствующая команда exec в строке 8. Она сообщает: "Замените исполняемый в настоящее время процесс (данный сценарий Bash) исполнением следующей команды" - почти так, как если бы данный сценарий никогда не существовал. Но какую программу запускает exec? Значение "$@" означает "все аргументы, предоставленные данному сценарию Bash, что в нашем случае является bin/rails s -b 0.0.0.0. Поэтому на самом деле мы велим "Заменить данный исполняемый сценарий Bash неким сервером Rails".

Суммируя, docker-entrypoint.sh действует как некий обёртывающий сценарий, предоставляющий нам возможность осуществить нашу небольшую очистку имеющегося файла pid и затем запустить наш сервер Rails как ни в чём не бывало. Теперь вы можете запускать docker-compose up как вашей душе угодно, зная что надоедливая ошибка не помешает вам.

Точки входа, в особенности при следовании обсуждённым шаблоном, являются хорошим инструментом для присутствия в вашем арсенале; вы может найти и иные творчекие варианты применения для них. Будет полезно также знать, что вы можете также определять также некую точку входа напрямую в самом файле Dockerfile, применяя соответствующую инструкцию ENTRYPOINT. Для получения дополнительных подробностей ознакомьтесь с документацией Docker.

Составляйте периодические прерывания при помощи Ctrl-C

Когда мы запускаем ваше приложение при помощи Compose в определяемом по умолчанию, присоединённом режиме, иными словами, без соответствующего параметра -d - Compose подключает к каждому контейнеру stdout, цепляя в хвост его вывод.

Когда вы нажимаете Ctrl-C, полагается что Compose выдаёт инструкцию своим контейнерам на прерывание, отправляя соответствующий сигнал SIGTERM своего основного процесса. Данный процесс должен выполнить аккуратный выход, после чего должен завершиться и сам контейнер. Когда это происходит верно, соответствующий вывод Compose на нажатие Ctrl-C таков:


​ 	Killing myapp_web_1 ... done
​ 	Gracefully stopping... (press Ctrl+C again to force)
		

Однако в моей практике, пожалуй, в 10- 50 процентов случаев вместо аккуратного завершения контейнеров мы получаем следующее:


​ 	^CERROR: Aborting.
		

И прекращение завершается неудачей, оставляя эти контейнеры всё ещё исполняющимися. Это не есть хорошо.

К сожалению, это выглядит как давно присутствующая, известная, проблема. По всей видимости, она вызывается проблемой в PyInstaller, неком инструменте с открытым исходным кодом для создания исполняемых файлов из сценариев Python, на который полагается Compose.

Основная проблема скорее в досаде, чем в самом представлении. Мы способны и вручную останавливать свои контейнеры вызывая команду docker-compose stop (или kill). Однако, хотя это и кажется проблемой сторонней зависимости, это не может не подрывать нашей уверенности в самом Compose, что является ужасной стыдобой.

Несмотря на то, что я исследовал данную проблему и пытался найти различные предлагаемые исправления, я не смог найти никакого обходного пути её решения. Если вы столкнулись с данной проблемой, мой совет состоит в том, чтобы просто не запускать ваши приложения в присоединённом режиме, а вместо этого всегда применять отсоединённый режим при помощи соответствующего параметра -d (detached). На сегодняшний день я не испытывал при этом данной проблемы при таком подходе.

Беглый обзор

Никакое программное обеспечение не бывает идеальным, но прискорбно когда ваша практика применения некого инструментария преуменьшается за счёт ошибок. Я должен признаться: мне было больно писать эту главу. Я бы хотел чтобы ваш опыт использования Docker был неизменно положительным.

В конце концов, я ощущал достаточную важность этих проблем чтобы привлечь к ним ваше внимание. Надеюсь, теперь вы осведомлены об основных существующих проблемах и готовы к столкновению с ними.

Давайте повторим что мы охватили в этой главе:

  1. Мы изучили некую проблему при которой файл tmp/pids/server.pid Rails не всегда удаляется в процессе прекращения своих контейнеров.

  2. Мы знакомились с точками входа, которые предваряют запуск соответствующей команды при старте некого нового контейнера.

  3. Мы воспользовались некой точкой входа для создания обёртывающего сценария, который удаляет имеющийся tmp/pids/server.pid при запуске контейнера, обходя данную проблему.

  4. Мы обсудили некую проблему, при которой Compose прерывается внутри себя вместо аккуратного прекращения контейнеров, придя к заключению, что наилучшим подходом для избегания этого будет запуск Compose в отсоединённом режиме (-d).

Ладно, хватит о грустном. Самое время подумать о положительных сторонах.

Заключительные мысли относительно Docker в разработке

Завершая данный посвящённый разработке раздел, давайте потратим немного времени на то, чтобы задуматься чего мы достигли. На первый взгляд легко допустить ошибку и посчитать что мы приложили немало усили чтобы вернуться туда, откуда мы начинали - к стандартному, работающему приложению Rails.

Тем не менее, на деле мы достигли некоторых основных преимуществ:

  • Наши файлы Dockerfile и docker-compose.yml снабдили нас декларативным описанием всего нашего приложения целиком - со всеми требующимися для него частями, такими как база данных - помогая нам представить ясную картину того что же делает наше приложение.

  • Мы способны раскрутить своё приложение единственной командой - даже если ничего перед этим не установлено. Docker выполняет выгрузку и установку всего что нам потребуется.

  • Мы прекратили необходимость вручную устанавливать зависимости программного обеспечения своего основного приложения в нашей локальной машине. Больше никаких пустяков с получением Redis, Postgres или даже Ruby установленными и исполняемыми с совместимыми версиями по всей своей команде целиком. За нас обо всём этом заботится Docker.

  • Этот последний момент сам по себе значим. Он также означает, что наше прикладное приложение может исполняться в любой машине с установленным Docker. Это предоставляет свободу и переносимость.

  • Обновление частей нашего приложения настолько же простое, как и обновление значения номера версии того образа, на который мы ссылаемся в своём файле Compose. Например, запросто обнаружить, например, как наше приложение работает в более новой версии Ruby.

По всем этим причинам использование Docker в разработке является полезным и самим по себе - вы должны ощущать гордость достигнув этого промежуточного камня. Однако наше путешествие не заканчивается здесь. Docker может привнести ещё больше преимуществ для дальнейшего продвижения в промышленность.

{Прим. пер.: к сожалению, здесь наши интересы расходятся. Нас зачаровало данное описание практики работы с Docker. Тем не менее, наше видение дальнейшего внедрения получаемых приложений Docker связано с применением Kubernetes, для которого мы пока не встречали руководство по практике работы с контейнерами подобного уровня. Если же вы, читатели, полагаете что данный перевод следует продолжить, обращайтесь к нам.}