Docker Compose

В экосистеме Докера есть несколько других инструментов с открытым исходным кодом, которые хорошо взаимодействуют с Докером. Некоторые из них:

  1. Docker Machine позволяет создавать Докер-хосты на своем компьютере, облачном провайдере или внутри дата-центра.
  2. Docker Compose — инструмент для определения и запуска много-контейнерных приложений.
  3. Docker Swarm — нативное решение для кластеризации.

В этом разделе мы поговорим об одном из этих инструментов — 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

Как замапить имя хоста на IP-адрес

Сразу скажу, что трюк не работает с 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 который возможно приравнивается к деплой-режиму (см. сноску на официальном сайте)

Проброс SSH-агента

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'

Как обратиться к IP-адресу машины, на которой запущен докер

version: '3.5'

services:
    php-fpm:
        extra_hosts:
            - "host.docker.internal:host-gateway"

Теперь если внутри контейнера обратиться к домену host.docker.internal то запрос уйдет на IP-адрес машины, на которой запущен докер.

PHP Xdebug 3.*

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"

PHP Xdebug 2.*

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

Штобы расшарить юдп-порт, просто нужно к порту дописать /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

Удачки.


27.09.2016 08:29