О Docker

2015-04-19

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

Docker logo

Завел для локалхоста такое правило. Всякие инструменты разработки, гуй, компиляторы-шмапиляторы ставятся на локалхост. Как обычно. А вот все сервера, будь то какой-нибудь сервер БД, или веб сервер, или еще какой сервер, запускаются только в Докере.

Докер — это такой модный, молодежный и даже хипстерский способ работы с линукс контейнерами (они же LXC). Это такая легкая виртуализация, как, например, OpenVZ. Вы имеете одно ядро (своего локалхоста) на всех и суровую изоляцию процессов и файловой системы контейнера. Это все похоже на chroot (самый мягкий вариант), но скорее ближе к BSDшному jail или солярисовым зонам. Как-то так.

Зачем еще один инструмент управления контейнерами? Или, если вы, как и я, вообще с контейнерами, кроме chroot, дела не имели: Зачем вообще нужны эти контейнеры? Чем плохи виртуалки? Пусть даже такие попсовые, как VirtualBox.

Виртуалки, они сильно жирнее, чем контейнеры. Сколько там места на диске требует Убунту для установки? Минимум 6 гигабайт. Сколько ей для запуска графики и прочего нужно памяти? Ну хотя бы гигабайт. Что получается? Получается, что больше, ну, допустим, четырех виртуалок вы у себя на локалхосте не запустите.

А что такое Убунта для Докера? Это образ на 188 мегабайт. Ну и памяти он съест столько, сколько съедят запущенные в контейнере процессы.

Docker vs VMs

Вот тут начинается большая разница в использовании виртуалок и контейнеров. Виртуалки вы создаете на века. Чтобы бережно настраивать их. Обновлять в них софт. Ставить все до кучи в одну виртуалку. И БД, и веб сервер, и все, что нужно для приложения. Ну так уж и быть, иногда бывает, что и копируете виртуалку куда-нибудь соседу или на другой сервер. Деплоить виртуалки целиком? Ненене, вы что, мы ж не в облаке каком. (Ну в облаке по необходимости придется).

Контейнер создается, чтобы запустить один процесс. Один единственный. Зато бережно изолированный. Зато в тщательно подобранном окружении, с конкретными версиями только нужных либ. Ну в редких случаях парочку процессов.

И если процесс нам больше не нужен, мы убиваем контейнер. Возможно, то, что процесс наделал, нам нужно. Тогда мы создаем из остатков контейнера новый образ. Для создания новых контейнеров. А может, процесс сделал свое дело и все. Тогда мы без сожаления возвращаемся туда, откуда начали. И новые контейнеры создаем из оригинального образа.

Получается в Докере такой круговорот. Образ — контейнер — образ. И это вполне естественно и удобно.

Docker Hub

Образы берутся с Хаба. Это Докерхаб. Но можете думать о нем, как об аналоге Гитхаба. Только для образов.

Для всего более менее популярного есть образы. Убунта, Монга, ИнфлюксДБ с Графаной — пожалуйста.

Образы принято называть так.

юзер/образ:тег

Юзер — это зарегистрированный юзер Хаба. Очень официальные образы чего-нибудь очень популярного не имеют этой части имени. Образ — это имя образа, одно или несколько слов через дефис. Тег — это метка образа. Убунты и всякие монги бывают разных версий, соответственно, для разных версий и разные теги. Теги часто имеют много синонимов, например, ubuntu:trusty и ubunutu:14.04 — это одно и то же. Последняя стабильная версия софта всегда помечается тегом :latest.

Образы слоистые. Как людоеды. Образы под одним и тем же названием и тегом могут изменяться. Софт может обновляться или еще что. Изменения, я полагаю, происходят в результате выполнения того самого цикла образ — контейнер — образ. Каждый новый слой контейнера — это дифф от его предыдущего состояния. В смысле изменений файлов и метаданных контейнера.

На самом деле, думайте о слоях образов, как о коммитах. А о тегах, как о тегах. Тут работает полная аналогия с (распределенными) системами контроля версий. Вот эта вот версионная система образов, с публичным репозиторием, и делает Докер тем самым Докером.

Ogre layers

Ну давайте возьмем образ Убунты и сделаем с ним что-нибудь.

$ docker pull ubuntu:latest

Рекомендую всегда указывать тут тег, а то еще выкачает туеву хучу совсем не нужных вам версий образов.

Теперь у вас где-то локально есть образ Убунты. Можно убедиться в этом.

$ docker images
REPOSITORY  TAG     IMAGE ID      CREATED      VIRTUAL SIZE
ubuntu      latest  d0955f21bf24  4 weeks ago  188.3 MB

Теперь создадим из образа новенький контейнер и запустим в нем баш.

$ docker run -i -t ubuntu bash

Ключ -i говорит о том, что мы хотим общаться с процессом в контейнере интерактивно. Ключ -t требует создание виртуального tty для контейнера, иначе интерактивности не получится. Всегда используйте -i и -t вместе. Далее указывается имя образа: ubuntu и запускаемая в контейнере команда: bash. Можно далее указать и параметры для этой команды, тут имеется аналогия с ssh.

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

# apt-get install nginx

При желании можно поправить его конфигурацию. Вима и Емакса в образе нет. Ну давайте хоть nano поставим.

# apt-get install nano
# nano /etc/nginx/sites-available/default

В этом файле ничего менять не надо. Пока лишь запомним, что nginx слушает 80 порт и корень дефолтного сайта он ищет по пути /usr/share/nginx/html.

Однако, нам обязательно надо поменять /etc/nginx/nginx.conf, добавить в него строчку

daemon off;

Как-то так.

# echo "daemon off;" >> /etc/nginx/nginx.conf

Nginx поставили? Сохранили конфиг? А теперь выходим из консоли. Жмем Ctrl+D.

Упс. Контейнер схлопнулся. Неожиданно, после виртуалок-то. На самом деле все правильно. Процесс, ради которого создавался контейнер, в данном случае баш, завершился. Ну и контейнер должен завершить свою работу. Поэтому мы и добавили daemon off, чтобы nginx не завершал свой стартовый процесс и Докер продолжал бы работу с nginx в качестве основного процесса.

Посмотрим на останки контейнера.

$ docker ps -a
CONTAINER ID  IMAGE          COMMAND  CREATED         STATUS                     PORTS  NAMES
5bf7094e6961  ubuntu:latest  bash     10 minutes ago  Exited (0) 27 seconds ago         silly_darwin

Вот он, родимый, с веселым именем silly_darwin. Докер так забавно раздает случайные имена.

Что мы в этом контейнере наделали?

$ docker diff silly_darwin
A /.bash_history
C /bin
A /bin/nano
A /bin/rnano
C /etc/default
A /etc/default/nginx
...

Ну все правильно, добавились файлы пакетов nginx и nano, с зависимостями. Нам надо сохранить эти изменения для последующего использования. В образе. Замыкаем цикл образ — контейнер — образ.

$ docker commit silly_darwin gelin/test-nginx

Мы добавили свой собственный слой поверх штатного образа Убунты. И сохранили его локально под именем test-nginx. Как новый образ.

Давайте теперь запустим наш сервер. Сервер у нас сетевой. И, как мы помним, он слушает порт 80. В контейнере. Чтобы достучаться до сети в контейнере, нам нужно указать маппинг порта. Пусть порт 8080 нашего локалхоста мапится на 80 порт в контейнере. Нам поможет ключ -p.

$ docker run -d --name test -p 8080:80 gelin/test-nginx nginx

Мы тут явно указали имя нашего контейнера: test. А еще указали ключ -d, чтобы запущенный докер ушел в фон. Теперь сервер действительно запущен как сервер.

$ docker ps
CONTAINER ID  IMAGE                    COMMAND  CREATED        STATUS        PORTS                 NAMES
4fe62f3690c7  gelin/test-nginx:latest  nginx    5 seconds ago  Up 5 seconds  0.0.0.0:8080->80/tcp  test

И по адресу http://localhost:8080 локалхоста должна открываться стартовая страница нашего контейнерного nginxа.

Создадим-ка другую стартовую страничку. Нарисуем её в чем угодно и положим в файлик ~/tmp/index.html нашего ненаглядного локалхоста. Как её теперь передать нашему nginxу?

Можно, конечно, снова запустить баш и модифицировать /usr/share/nginx/html. Закоммитить и создать новый образ. Повторить цикл.

Но есть более интересный способ для случая, когда у нас есть внешние для контейнера данные, которые нужно в него подсунуть. Это так же касается и баз данных, например, можно подсунуть файлы самих данных контейнеру. Задействуем ключик -v и скажем, что ~/tmp/index.html в файловой системе локалхоста должен оказаться в /usr/share/nginx/html файловой системы контейнера.

$ docker run -d --name test -p 8080:80 -v ~/tmp/index.html:/usr/share/nginx/html/index.html gelin/test-nginx nginx
2015/04/19 10:34:50 Error response from daemon: Conflict, The name test is already assigned to 4fe62f3690c7. You have to delete (or rename) that container to be able to assign test to a container again.

Ахда. Контейнер с именем test у нас все еще запущен. Он нам уже не нужен. Давайте убъем его и подчистим останки.

$ docker stop test
$ docker rm test

Снова запускаем.

$ docker run -d --name test -p 8080:80 -v ~/tmp/index.html:/usr/share/nginx/html/index.html gelin/test-nginx nginx

Смотрим, изменилась ли страничка на http://localhost:8080. Ура, изменилась! Можно даже поправить файлик index.html и убедиться, что nginx его корректно подхватит.

Ключик -v работает с так называемыми томами (volumes). Файловая система хоста и контейнера объединяются так что файлы хоста замещают файлы контейнера.

А что, если наш nginx не должен быть публичным сервером? Что если он скрывает некоторое API, которое нужно другому контейнеру в нашей микросервисной архитектуре, распиханной по контейнерам?

Нам нужны линки между контейнерами. Запустим еще одну Убунту, но с ключиком --link.

$ docker run -i -t --link test:nginx ubuntu bash

Мы попросили Докер слинковать этот новый контейнер с другим контейнером по имени test. Мы не зря давали имя явно. И этот test все еще запущен в фоне. Внутри нового контейнера этот test будет виден под алиасом nginx.

Что это значит? А вот что. Нам виден хост по имени nginx.

# ping nginx
PING nginx (172.17.0.26) 56(84) bytes of data.
64 bytes from nginx (172.17.0.26): icmp_seq=1 ttl=64 time=0.200 ms

На самом деле эта магия заключается в автоматическом переписывании Докером /etc/hosts.

# cat /etc/hosts
172.17.0.26 nginx

Соответственно, 80 порт этого хоста вполне себе доступен.

# apt-get install curl
# curl http://nginx/ -v
* Hostname was NOT found in DNS cache
*   Trying 172.17.0.26...
* Connected to nginx (172.17.0.26) port 80 (#0)
...

А еще есть переменные окружения, сообщающие о том, что за хосты нам доступны и какие порты там открыты.

# env
NGINX_PORT_80_TCP_PROTO=tcp
NGINX_PORT_80_TCP=tcp://172.17.0.26:80
NGINX_PORT_80_TCP_PORT=80
NGINX_PORT_80_TCP_ADDR=172.17.0.26
NGINX_PORT=tcp://172.17.0.26:80

Можно использовать их во всякоразных скриптах.

Получившися образ, если он удался, полезен для других, нужно развернуть где-то еще, можно запушить на Хаб.

$ docker push gelin/test-nginx

Сначала, конечно, нужно завести там аккаунт и залогинить Докер.

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

Docker workflow