О Docker
2015-04-19
Кажется, я осилил Docker в достаточно хомячковой степени, чтобы написать об этом. В достаточной степени, чтобы использовать его на локалхосте, и не доставлять мучений окружающим.
Завел для локалхоста такое правило. Всякие инструменты разработки, гуй, компиляторы-шмапиляторы ставятся на локалхост. Как обычно. А вот все сервера, будь то какой-нибудь сервер БД, или веб сервер, или еще какой сервер, запускаются только в Докере.
Докер — это такой модный, молодежный и даже хипстерский способ работы с линукс контейнерами (они же LXC). Это такая легкая виртуализация, как, например, OpenVZ. Вы имеете одно ядро (своего локалхоста) на всех и суровую изоляцию процессов и файловой системы контейнера. Это все похоже на chroot (самый мягкий вариант), но скорее ближе к BSDшному jail или солярисовым зонам. Как-то так.
Зачем еще один инструмент управления контейнерами? Или, если вы, как и я, вообще с контейнерами, кроме chroot, дела не имели: Зачем вообще нужны эти контейнеры? Чем плохи виртуалки? Пусть даже такие попсовые, как VirtualBox.
Виртуалки, они сильно жирнее, чем контейнеры. Сколько там места на диске требует Убунту для установки? Минимум 6 гигабайт. Сколько ей для запуска графики и прочего нужно памяти? Ну хотя бы гигабайт. Что получается? Получается, что больше, ну, допустим, четырех виртуалок вы у себя на локалхосте не запустите.
А что такое Убунта для Докера? Это образ на 188 мегабайт. Ну и памяти он съест столько, сколько съедят запущенные в контейнере процессы.
Вот тут начинается большая разница в использовании виртуалок и контейнеров. Виртуалки вы создаете на века. Чтобы бережно настраивать их. Обновлять в них софт. Ставить все до кучи в одну виртуалку. И БД, и веб сервер, и все, что нужно для приложения. Ну так уж и быть, иногда бывает, что и копируете виртуалку куда-нибудь соседу или на другой сервер. Деплоить виртуалки целиком? Ненене, вы что, мы ж не в облаке каком. (Ну в облаке по необходимости придется).
Контейнер создается, чтобы запустить один процесс. Один единственный. Зато бережно изолированный. Зато в тщательно подобранном окружении, с конкретными версиями только нужных либ. Ну в редких случаях парочку процессов.
И если процесс нам больше не нужен, мы убиваем контейнер. Возможно, то, что процесс наделал, нам нужно. Тогда мы создаем из остатков контейнера новый образ. Для создания новых контейнеров. А может, процесс сделал свое дело и все. Тогда мы без сожаления возвращаемся туда, откуда начали. И новые контейнеры создаем из оригинального образа.
Получается в Докере такой круговорот. Образ — контейнер — образ. И это вполне естественно и удобно.
Образы берутся с Хаба. Это Докерхаб. Но можете думать о нем, как об аналоге Гитхаба. Только для образов.
Для всего более менее популярного есть образы. Убунта, Монга, ИнфлюксДБ с Графаной — пожалуйста.
Образы принято называть так.
юзер/образ:тег
Юзер — это зарегистрированный юзер Хаба.
Очень официальные образы чего-нибудь очень популярного не имеют этой части имени.
Образ — это имя образа, одно или несколько слов через дефис.
Тег — это метка образа.
Убунты и всякие монги бывают разных версий,
соответственно,
для разных версий и разные теги.
Теги часто имеют много синонимов,
например, ubuntu:trusty
и ubunutu:14.04
— это одно и то же.
Последняя стабильная версия софта всегда помечается тегом :latest
.
Образы слоистые. Как людоеды. Образы под одним и тем же названием и тегом могут изменяться. Софт может обновляться или еще что. Изменения, я полагаю, происходят в результате выполнения того самого цикла образ — контейнер — образ. Каждый новый слой контейнера — это дифф от его предыдущего состояния. В смысле изменений файлов и метаданных контейнера.
На самом деле, думайте о слоях образов, как о коммитах. А о тегах, как о тегах. Тут работает полная аналогия с (распределенными) системами контроля версий. Вот эта вот версионная система образов, с публичным репозиторием, и делает Докер тем самым Докером.
Ну давайте возьмем образ Убунты и сделаем с ним что-нибудь.
$ 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
Сначала, конечно, нужно завести там аккаунт и залогинить Докер.
И кто-то другой, на другом локалхосте, сможет получить этот образ и запустить ваше приложение в точности в той же конфигурации, которую вы создали. Вот для этого и нужны контейнеры.