В экосистеме Докера есть несколько других инструментов с открытым исходным кодом, которые хорошо взаимодействуют с Докером. Некоторые из них:
В этом разделе мы поговорим об одном из этих инструментов — Docker Compose, и узнаем, как он может упростить работу с несколькими контейнерами.
У Docker Compose довольно интересная предыстория. Примерно два года назад компания OrchardUp запустила инструмент под названием Fig. Идея была в том, чтобы создавать изолированные рабочие окружения с помощью Докера. Проект очень хорошо восприняли на Hacker News - я смутно помню, что читал о нем, но не особо понял его смысла.
Первый комментарий на самом деле неплохо объясняет, зачем нужен Fig и что он делает:
На самом деле, смысл Докера в следующем: запускать процессы. Сегодня у Докера есть неплохое API для запуска процессов: расшаренные между контейнерами (иными словами, запущенными образами) разделы или директории (shared volumes), перенаправление портов с хост-машины в контейнер, вывод логов, и так далее. Но больше ничего: Докер сейчас работает только на уровне процессов.
Не смотря на то, что в нем содержатся некоторые возможности оркестрации нескольких контейнеров для создания единого "приложения", в Докере нет ничего, что помогало бы с управлением такими группами контейнеров как одной сущностью. И вот зачем нужен инструмент вроде Fig: чтобы обращаться с группой контейнеров как с единой сущностью. Чтобы думать о "запуске приложений" (иными словами, "запуске оркестрированного кластера контейнеров") вместо "запуска контейнеров".
Оказалось, что многие пользователи Докера согласны с такими мыслями. Постепенно, Fig набрал популярность, Docker Inc. заметили, купили компанию и назвали проект Docker Compose.
Итак, зачем используется Compose? Это инструмент для простого определения и запуска многоконтейнерных Докер-приложений. В нем есть файл docker-compose.yml, и с его помощью можно одной командой поднять приложение с набором сервисов.
Давайте посмотрим, сможем ли мы создать файл docker-compose.yml для нашего приложения SF-Foodtrucks и проверим, способен ли он на то, что обещает.
Но вначале нужно установить Docker Compose. Есть у вас Windows или Mac, то Docker Compose уже установлен — он идет в комплекте с Docker Toolbox. На Linux можно установить Docker Compose следуя простым инструкциям на сайте документации. Compose написан на Python, поэтому можно сделать просто pip install docker-compose
Проверить работоспособность так:
$ docker-compose version
docker-compose version 1.7.1, build 0a9ab35
docker-py version: 1.8.1
CPython version: 2.7.9
OpenSSL version: OpenSSL 1.0.1j 15 Oct 2014
Теперь можно перейти к следующему шагу, то есть созданию файла docker-compose.yml. Синтаксис yml-файлов очень простой, и в репозитории уже есть пример, который мы будем использовать
version: "2"
services:
es:
image: elasticsearch
web:
image: prakhar1989/foodtrucks-web
command: python app.py
ports:
- "5000:5000"
volumes:
- .:/code
Давайте я разберу это подробнее. На родительском уровне мы задали название неймспейса для наших сервисов: es и web. К каждому сервису можно добавить дополнительные параметры, среди которых image — обязательный. Для es мы указываем доступный на Docker Hub образ elasticsearch. Для Flask-приложения — тот образ, который мы создали самостоятельно в начале этого раздела.
Замечание: нужно находиться в директории с файлом docker-compose.yml или можно указать путь к файлу, например:
docker-compose -f my-docker-compose.yml ...
Подробнее о параметрах и возможных значениях можно прочитать в документации.
Отлично! Файл готов, давайте посмотрим на docker-compose в действии. Но вначале нужно удостовериться, что порты свободны. Так что если у вас запущены контейнеры Flask и ES, то пора их остановить:
$ docker stop $(docker ps -q)
39a2f5df14ef
2a1b77e066e6
Теперь можно запускать docker-compose. Перейдите в директорию с приложением Foodtrucks и выполните команду docker-compose up.
$ docker-compose up
Creating network "foodtrucks_default" with the default driver
Creating foodtrucks_es_1
Creating foodtrucks_web_1
Attaching to foodtrucks_es_1, foodtrucks_web_1
es_1 | [2016-01-11 03:43:50,300][INFO ][node ] [Comet] version[2.1.1], pid[1], build[40e2c53/2015-12-15T13:05:55Z]
es_1 | [2016-01-11 03:43:50,307][INFO ][node ] [Comet] initializing ...
es_1 | [2016-01-11 03:43:50,366][INFO ][plugins ] [Comet] loaded [], sites []
es_1 | [2016-01-11 03:43:50,421][INFO ][env ] [Comet] using [1] data paths, mounts [[/usr/share/elasticsearch/data (/dev/sda1)]], net usable_space [16gb], net total_space [18.1gb], spins? [possibly], types [ext4]
es_1 | [2016-01-11 03:43:52,626][INFO ][node ] [Comet] initialized
es_1 | [2016-01-11 03:43:52,632][INFO ][node ] [Comet] starting ...
es_1 | [2016-01-11 03:43:52,703][WARN ][common.network ] [Comet] publish address: {0.0.0.0} is a wildcard address, falling back to first non-loopback: {172.17.0.2}
es_1 | [2016-01-11 03:43:52,704][INFO ][transport ] [Comet] publish_address {172.17.0.2:9300}, bound_addresses {[::]:9300}
es_1 | [2016-01-11 03:43:52,721][INFO ][discovery ] [Comet] elasticsearch/cEk4s7pdQ-evRc9MqS2wqw
es_1 | [2016-01-11 03:43:55,785][INFO ][cluster.service ] [Comet] new_master {Comet}{cEk4s7pdQ-evRc9MqS2wqw}{172.17.0.2}{172.17.0.2:9300}, reason: zen-disco-join(elected_as_master, [0] joins received)
es_1 | [2016-01-11 03:43:55,818][WARN ][common.network ] [Comet] publish address: {0.0.0.0} is a wildcard address, falling back to first non-loopback: {172.17.0.2}
es_1 | [2016-01-11 03:43:55,819][INFO ][http ] [Comet] publish_address {172.17.0.2:9200}, bound_addresses {[::]:9200}
es_1 | [2016-01-11 03:43:55,819][INFO ][node ] [Comet] started
es_1 | [2016-01-11 03:43:55,826][INFO ][gateway ] [Comet] recovered [0] indices into cluster_state
es_1 | [2016-01-11 03:44:01,825][INFO ][cluster.metadata ] [Comet] [sfdata] creating index, cause [auto(index api)], templates [], shards [5]/[1], mappings [truck]
es_1 | [2016-01-11 03:44:02,373][INFO ][cluster.metadata ] [Comet] [sfdata] update_mapping [truck]
es_1 | [2016-01-11 03:44:02,510][INFO ][cluster.metadata ] [Comet] [sfdata] update_mapping [truck]
es_1 | [2016-01-11 03:44:02,593][INFO ][cluster.metadata ] [Comet] [sfdata] update_mapping [truck]
es_1 | [2016-01-11 03:44:02,708][INFO ][cluster.metadata ] [Comet] [sfdata] update_mapping [truck]
es_1 | [2016-01-11 03:44:03,047][INFO ][cluster.metadata ] [Comet] [sfdata] update_mapping [truck]
web_1 | * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Перейдите по IP чтобы увидеть приложение. Круто, да? Всего лишь пара строк конфигурации и несколько Докер-контейнеров работают в унисон. Давайте остановим сервисы и перезапустим в detached mode:
web_1 | * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Killing foodtrucks_web_1 ... done
Killing foodtrucks_es_1 ... done
$ docker-compose up -d
Starting foodtrucks_es_1
Starting foodtrucks_web_1
$ docker-compose ps
Name Command State Ports
----------------------------------------------------------------------------------
foodtrucks_es_1 /docker-entrypoint.sh elas ... Up 9200/tcp, 9300/tcp
foodtrucks_web_1 python app.py Up 0.0.0.0:5000->5000/tcp
Не удивительно, но оба контейнера успешно запущены. Откуда берутся имена? Их Compose придумал сам. Но что насчет сети? Его Compose тоже делаем сам? Хороший вопрос, давайте выясним.
Для начала, остановим запущенные сервисы. Их всегда можно вернуть одной командой:
$ docker-compose stop
Stopping foodtrucks_web_1 ... done
Stopping foodtrucks_es_1 ... done
Заодно, давайте удалим сеть foodtrucks, которую создали в прошлый раз. Эта сеть нам не потребуется, потому чтоCompose автоматически сделает все за нас.
$ docker network rm foodtrucks
$ docker network ls
NETWORK ID NAME DRIVER
4eec273c054e bridge bridge
9347ae8783bd none null
54df57d7f493 host host
Класс! Теперь в этом чистом состоянии можно проверить, способен ли Compose на волшебство.
$ docker-compose up -d
Recreating foodtrucks_es_1
Recreating foodtrucks_web_1
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f50bb33a3242 prakhar1989/foodtrucks-web "python app.py" 14 seconds ago Up 13 seconds 0.0.0.0:5000->5000/tcp foodtrucks_web_1
e299ceeb4caa elasticsearch "/docker-entrypoint.s" 14 seconds ago Up 14 seconds 9200/tcp, 9300/tcp foodtrucks_es_1
Пока все хорошо. Проверим, создались ли какие-нибудь сети:
$ docker network ls
NETWORK ID NAME DRIVER
0c8b474a9241 bridge bridge
293a141faac3 foodtrucks_default bridge
b44db703cd69 host host
0474c9517805 none null
Видно, что Compose самостоятельно создал сеть foodtrucks_default и подсоединил оба сервиса в эту сеть, так, чтобы они могли общаться друг с другом. Каждый контейнер для сервиса подключен к сети, и оба контейнера доступны другим контейнерам в сети. Они доступны по hostname, который совпадает с названием контейнера. Давайте проверим, находится ли эта информация в /etc/hosts.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bb72dcebd379 prakhar1989/foodtrucks-web "python app.py" 20 hours ago Up 19 hours 0.0.0.0:5000->5000/tcp foodtrucks_web_1
3338fc79be4b elasticsearch "/docker-entrypoint.s" 20 hours ago Up 19 hours 9200/tcp, 9300/tcp foodtrucks_es_1
$ docker exec -it bb72dcebd379 bash
root@bb72dcebd379:/opt/flask-app# cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.18.0.2 bb72dcebd379
Упс! Оказывается, файл понятия не имеет о es. Как же наше приложение работает? Давайте попингуем его по названию хоста:
root@bb72dcebd379:/opt/flask-app# ping es
PING es (172.18.0.3) 56(84) bytes of data.
64 bytes from foodtrucks_es_1.foodtrucks_default (172.18.0.3): icmp_seq=1 ttl=64 time=0.049 ms
64 bytes from foodtrucks_es_1.foodtrucks_default (172.18.0.3): icmp_seq=2 ttl=64 time=0.064 ms
^C
--- es ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.049/0.056/0.064/0.010 ms
Вуаля! Работает! Каким-то магическим образом контейнер смог сделать пинг хоста es. Оказывается, Docker 1.10 добавили новую сетевую систему, которая производит обнаружение сервисов через DNS-сервер. Если интересно, то почитайте подробнее о предложении и release notes.
На этом наш тур по Docker Compose завершен. С этим инструментом можно ставить сервисы на паузу, запускать отдельные команды в контейнере и даже масштабировать систему, то есть увеличивать количество контейнеров. Также советую изучать некоторые другие примеры использования Docker Compose.
Удалить все докер-сети можно так:
docker network prune
Сразу скажу, что трюк не работает с localhost, но пример буду показывать на нем, чтобы Вы тоже были в курсе:
version: '3.5'
services:
nginx:
image: registry.site.ru/seo-nginx:c3db38f46f7
ports:
- 8080:80
extra_hosts:
- "localhost:172.20.0.2"
php-fpm:
image: registry.site.ru/seo-php-fpm:c3db38f46f7
networks:
frontend:
ipv4_address: 172.20.0.2
networks:
frontend:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
Убедимся, что IP адрес выдан правильно:
docker inspect my_php-fpm_1 | grep -A 1 -B 1 "172.20.0.2"
"IPAMConfig": {
"IPv4Address": "172.20.0.2"
},
--
"Gateway": "172.20.0.1",
"IPAddress": "172.20.0.2",
"IPPrefixLen": 24,
Удостоверимся, что localhost привязан правильно:
docker inspect my_nginx_1 | grep -A 1 -B 1 localhost
"ExtraHosts": [
"localhosti:172.20.0.2"
],
Заодно заглянем в контейнер:
docker-compose --compatibility exec nginx cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.20.0.2 localhost
172.18.0.3 29df2b26b29d
Вроде бы все хорошо, но если пингануть, то localhost резолвится неверно:
/var/www # ping localhost
PING localhost (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.062 ms
64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.078 ms
^C
--- localhost ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.062/0.070/0.078 ms
Увы, да ах :)
Сразу оговорюсь, к сожалению ни один из них так и не заработал с localhost
version: '3.5'
services:
nginx:
image: registry.site.ru/seo-nginx:c3db38f46f7
ports:
- 8080:80
links:
- "php-fpm:localhost"
external_links:
- localhost:php-fpm
network_mode: host
Вполне возможно, из-за того что я использую флаг --compatibility
который возможно приравнивается к деплой-режиму (см. сноску на официальном сайте)
version: '3'
services:
app:
container_name: yourcontainer
environment:
- SSH_AUTH_SOCK=/ssh-agent
image: yourapp
volumes:
- ${SSH_AUTH_SOCK}:/ssh-agent
Важно: если Вы в контейнере будете под пользователем root или под пользователем от которого запускаете docker-compse, то все будет хорошо, но если Вы войдете под другим пользователем, например www-data, то у ssh-агент будет вести себя так, словно никаких ключей не знает, и вот почему:
$ ssh-add -L
Error connecting to agent: Permission denied
Оказывается ssh-клиент не может подключиться к ssh-агенту по адресу указанному в env-переменной SSH_AUTH_SOCK
Проблема в том, что когда мы пробрасываем ssh-сокет (файл сокета), то доступы сохраняются, выполним в докере:
$ ls -la /ssh-agent
srwxrwxr-x 1 1000 1000 0 Aug 18 04:52 /ssh-agent
Видим, что нет доступа на запись (а он почему-то очень нужен ssh-клиенту), поэтому выполняем в контейнере:
chmod 777 /ssh-agent
или выполняем на хост-машине:
sudo chmod 777 /run/user/1000/keyring/ssh
и заходим в контейнер под нужным нам пользователем (в моем случае под пользователем www-data):
docker-compose exec --user=www-data php-fpm sh
Заметка: адрес /run/user/1000/keyring/ssh
я узнал просто выполнив на хост-машине команду:
$ echo $SSH_AUTH_SOCK
docker-compose run -v /real/entrypoint.sh:/in-docker/entrypoint.sh --entrypoint=/in-docker/entrypoint.sh php-fpm sh
docker-compose run --entrypoint= php-fpm sh
docker-compose -f my.yml up
version: '3.5'
services:
php-fpm:
entrypoint: php-fpm
user: 'www-data'
Но обычно я делаю так:
docker-compose exec --user=www-data php-fpm bash
А вот так можно по-умолчанию входить в контейнер под НЕСУЩЕСТВУЮЩИМ в контейнере пользователем:
version: '3.5'
services:
php-fpm:
entrypoint: php-fpm
user: '1000:1000'
version: '3.5'
services:
php-fpm:
extra_hosts:
- "host.docker.internal:host-gateway"
Теперь если внутри контейнера обратиться к домену host.docker.internal
то запрос уйдет на IP-адрес машины, на которой запущен докер.
version: '3.5'
services:
php-fpm:
environment:
- SSH_AUTH_SOCK=/ssh-agent
- PHP_IDE_CONFIG=serverName=common
- XDEBUG_SESSION=common
- XDEBUG_MODE=debug
- XDEBUG_CONFIG=max_nesting_level=200 client_port=9003 client_host=host.docker.internal
extra_hosts:
- "host.docker.internal:host-gateway"
version: '3.5'
services:
php-fpm:
environment:
- XDEBUG_HOST=172.16.30.130
- XDEBUG_PORT=9003
- PHP_IDE_CONFIG=serverName=127.0.0.1
В документе рассказано про "Сборка согласно цели" и чтобы сделать такую сборку в docker-compose нужно:
version: '3.5'
services:
nginx:
build:
context: .
dockerfile: docker-files/prod/nginx/Dockerfile
target: dev
В документе рассказано про "Монтирование других контейнеров" и чтобы сделать такую сборку в docker-compose нужно:
version: '3.5'
services:
nginx:
build:
context: .
args:
# Образ nginx строится на основе docker-compose образа php-fpm
- PHP_FPM_IMAGE=php-fpm-image-name-for-build-nginx:latest
dockerfile: docker-files/prod/nginx/Dockerfile
Dockerfile
version: '3.5'
services:
php-fpm:
build:
context: .
dockerfile: docker/php-fpm/Dockerfile
image: my-php-fpm-image-name
version: '3.5'
services:
php-fpm:
working_dir: /app
Штобы расшарить юдп-порт, просто нужно к порту дописать /udp
version: '3'
services:
logstash:
image: logstash:8.12.2
ports:
- 515:515/udp
Проверить работу можно командой docker compose ps
:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
elasticsearch-logstash-1 logstash:8.12.2 "/usr/local/bin/dock…" logstash 8 minutes ago Up 8 minutes 0.0.0.0:515->515/udp, :::515->515/udp
Как видим: 0.0.0.0:515->515/udp, :::515->515/udp
Удачки.