Предистория


Одним прекрасным днём мне понадобилось развернуть среду разработки для своего проекта. Vagrant уже порядком поднадоел и хотелось иметь единую среду разработки для всех участников проекта которая была бы идентичной production серверу. Соответственно наслушавшись информации про хипстерский docker, я решил начать с ним разбираться. Далее я постараюсь максимально подробно описать все шаги начиная от установки докера на локалке вплоть до разворачивания продуктива на KVM.

Исходный стек технологий:

— Docker
— Symfony 4
— nginx
— php-fpm
— postgresql
— elasticsearch
— rabbitmq
— jenkins

Железо:

— ноутбук под ОС Ubuntu 16.04
— продакшн сервер на хостинге KVM

Почему кроме технологического стека я перечислил ещё и стек железа?

Если вы никогда ранее не работали с докером, то вы можете столкнуться с рядом проблем, связанных именно с железом, операционной системой вашего ноутбука или типом виртуализации на хостинге.

Первый и наверно самый важный аспект при начале работы с докером — это операционная система вашего ноутбука. Проще всего работать с докером именно на linux системах. Если вы работаете на Windows или Mac то у вас 100 % будут некоторые сложности, но эти сложности не будут являться критическими и при желании «нагуглить» как это исправляется не составит никаких проблем.

Второй вопрос — это хостинг. Зачем нужен Hosting именно с типом виртуализации KVM? Причина в том, что виртуализация VPS разительно отличается от KVM и установить сам docker на VPS у вас попросту не выйдет, так как VPS распределяет ресурсы сервера динамически.

Подитог: для самого быстрого старта на докере резоннее всего выбирать Ubuntu в качестве локальной операционки и KVM хостинг (либо собственный сервер). Далее рассказ пойдёт опираясь именно на эти две составляющие.

Docker-compose для локалки


Установка


Для начала необходимо установить локально сам докер. Инструкцию по установке можно посмотреть на официальном сайте ссылка на официальную документацию для ubuntu (необходимо установить docker и docker-compose), либо запустив команду в консоли:

curl -sSl https://get.docker.com/ | sh

Эта команда установит и docker и docker-compose. После этого проверить версию докера можно командой:

docker --version

Я все это дело запускаю на докере версии 18.06.0-ce.

Установка закончена!

Осознание


Чтобы с чем-то работать более менее успешно — необходимо иметь представление как оно работает. Если вы ранее работали только с Vagrant или чем-либо похожим, то будет крайне непривычно и непонятно поначалу, но это лишь поначалу.

Я постараюсь провести аналогию к Vagrant. Сейчас многие могут сказать, что сравнивать Vagrant и Docker коренным образом не верно. Да, я с этим согласен, но я их сравнивать и не собираюсь, я лишь попытаюсь донести до новичков, работавших только с Vagrant, систему работы Docker, апеллируя тем, что знают новички.

Мое видение контейнера «на пальцах» представляется так: каждый контейнер — это крохотный изолированный мирок. Каждый контейнер можно представить как будто бы это крохотный Vagrant на котором установлен всего 1 инструмент, например nginx или php. Изначально контейнеры изолированы вообще от всего вокруг, но путём не хитрых манипуляций можно настроить все так, чтобы они общались между собой и работали совместно. Это не значит, что каждый из контейнеров — это отдельная виртуальная машина, совсем нет. Но так проще для первоначального понимания, как мне кажется.

Vagrant просто откусывает у вашего компьютера часть ресурсов, создает виртуальную машину, устанавливает на нее операционную систему, устанавливает библиотеки, устанавливает все то, что вы прописали в скрипте после vagrant up. В конечном итоге это выглядит примерно так:

> Посмотреть схемку

Docker в свою очередь работает кардинально иначе. Он не создает виртуальных машин. Docker создает контейнеры (можете воспринимать пока что их как микро-виртуалки) со своей операционной системой Alpine и 1-3 библиотеками, которые необходимы для работы приложения, например php или nginx. При этом Docker не блокирует под себя ресурсы вашей системы, а просто использует их по мере необходимости. В конечном итоге, если проиллюстрировать, то это выглядеть будет примерно так:

> Посмотреть схемку

Каждый из контейнеров имеет образ из которого он создаётся. Подавляющая часть образов представляет из себя расширение другого образа, например Ubuntu xenial или Alpine или Debian, на которые сверху накатываются дополнительные драйверы и другие компоненты.

Мой первый образ был для php-fpm. Мой образ расширяет официальный образ php:7.2-fpm-alpine3.6. То есть по сути он берет официальный образ и доставляет на него нужные мне компоненты, например, pdo_pgsql, imagick, zip и прочее. Таким образом можно создать образ, который нужен именно вам. Если есть желание можете пользоваться тут.

С созданием образов все довольно просто на мой взгляд, если они сделаны на базе xenial например, но доставляют немного геморроя, если они сделаны на базе Alpine. До начала работы с докером я про Alpine в принципе и не слышал, так как Vagrant у меня всегда работал под Ubuntu xenial. Alpine представляет из себя пустую Linux операционную систему, в которой по сути вообще нет ничего (крайний минимум). Поэтому поначалу работать с ней крайне не удобно, так как нет например того же apt-get install (к которому так привыкаешь), а есть только apk add и не вполне вменяемый набор пакетов. Большой плюс Alpine заключается в его весе, например, если Xenial весит (абстрактно) 500мешков, то Alpine (абстрактно) порядка 78мешков. На что же это вообще влияет? А влияет это на скорость сборки и на конечный вес всех образов, которые будут храниться у вас на сервере в конечном итоге. Допустим, у вас 5 разных контейнеров и все на базе xenial суммарный их вес будет более 2,5 гигов, а alpine — порядка 500 мешков всего лишь. Поэтому в идеале надо стремиться к тому, чтобы контейнеры были как можно более худые. (Полезная ссылка для установки пакетов в Alpine — Пакеты Alpine).

На docker hub везде пишут как запускать контейнер используя команду docker run, и при этом почему-то не пишут как его можно запустить через docker-compose, а ведь именно через docker-compose он и будет запускаться большую часть времени, так как мало кому охота вручную запускать все контейнеры, сетки, порты открывать и прочее. Docker-compose от лица пользователя выглядит просто файл yaml с настройками. Он включает в себя описание каждого из сервисов, которые необходимо запустить. Моя сборка для локального окружения выглядит следующим образом:

version: '3.1'

services:

  php-fpm:
    image: otezvikentiy/php7.2-fpm:0.0.11
    ports:
      - '9000:9000'
    volumes:
      - ../:/app
    working_dir: /app
    container_name: 'php-fpm'

  nginx:
    image: nginx:1.15.0
    container_name: 'nginx'
    working_dir: /app
    ports:
      - '7777:80'
    volumes:
      - ../:/app
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf

  postgres:
    image: postgres:9.6
    ports:
      - '5432:5432'
    container_name: 'postgresql'
    working_dir: /app
    restart: always
    environment:
      POSTGRES_DB: 'db_name'
      POSTGRES_USER: 'db_user'
      POSTGRES_PASSWORD: 'db_pass'
    volumes:
      - ./data/dump:/app/dump
      - ./data/postgresql:/var/lib/postgresql/data

  rabbitmq:
    image: rabbitmq:3.7.5-management
    working_dir: /app
    hostname: rabbit-mq
    container_name: 'rabbit-mq'
    ports:
      - '15672:15672'
      - '5672:5672'
    environment:
      RABBITMQ_DEFAULT_USER: user
      RABBITMQ_DEFAULT_PASS: password
      RABBITMQ_DEFAULT_VHOST: my_vhost

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.3.0
    container_name: 'elastic-search'
    environment:
      - discovery.type=single-node
      - "discovery.zen.ping.unicast.hosts=elasticsearch"
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - 9200:9200
      - 9300:9300
    working_dir: /app
    volumes:
      - ../:/app
      - ./data/elasticsearch:/usr/share/elasticsearch/data

volumes:
  elasticsearch:
  postgresql:

docker-compose.yaml для SF4 представляет из себя определенный набор сервисов: nginx, php-fpm, postgresql, rabbitmq (если он вам нужен), elasticsearch (если он вам нужен). Для локального окружения этого хватит. Чтобы все это заработало — есть минимальный набор настроек, без которых ничего работать не будет. Чаще всего это image, volumes, ports, environment, working_dir и container_name. Все для запуска того или иного образа описано в его документации на hub.docker.com. Там не всегда есть описание для docker-compose, но это не значит, что оно с ним не работает. Просто необходимо перенести все входящие данные из команды docker run в docker-compose и все заработает.

Например, есть образ для RabbitMQ тут. Когда видишь ЭТО впервые — это вызывает смешанные чувства и эмоции, но не все так страшно. В Этом образе указаны тэги. Обычно тэги — представляют собой разные образы, разных версий приложения с различными расширяемыми образами. Например, тэг 3.7.7-alpine означает, что этот образ более тонкий, нежели чем, например, 3.7.7, так как он сделан на базе Alpine. Ну и так же в тэгах указываются чаще всего версии самого приложения. Я обычно выбираю наиболее свежую версию и стабильную версию самого приложения и образ alpine.

После того как вы изучили и выбрали тэг — далее зачастую вы видите что-то подобного рода:

docker run -d --hostname my-rabbit --name some-rabbit -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password rabbitmq:3-management

И первая мысль — WTF? Как перебросить это в docker-compose?

Все довольно не сложно. По факту, в этой строке указываются все те же параметры, что и в yaml файле, только сокращенные. Например, -e — это environment, в который передаются различные параметры, могут быть так же записи типа -p — это порты, которые в yaml называются ports. Соответственно, чтобы качественно использовать незнакомый образ, надо просто «загуглить» сокращения docker run команды и применять полные наименования в yaml файле.

Теперь вернемся в docker-compose.yml, который я привел в виде образца выше.

В данном примере используется мой образ для php7.2 сделанный как расширение для официального образа php7.2-fpm-alpine, но если вам не требуется такого количества дополнительных библиотек — то вы можете собрать своё расширение для официального образа и использовать его. Остальные образы для локалки у меня используются полностью оригинальные и официальные.

image — указываем какой образ скачать. Например (rabbitmq:3.7.7-management-alpine).

ports — указываем порты, которые будет использовать контейнер (см. документацию образа). Пример порт nginx это 80 по дефолту. Соответственно, если вы хотите использовать порт 80, то здесь необходимо указать 80:80 и ваш сайт будет доступен на localhost. Либо можно указать 7777:80, и тогда ваш сайт будет по url localhost:7777. Это необходимо бывает для того, чтобы несколько проектов можно было разворачивать на одном и том же хосте.

volumes — здесь указываются расшаренные директории. Например ваш проект лежит в директории ~/projects/my-sf4-app, а контейнер php настроен на работу с директорией /app (то же самое, что в варианте /var/www/my-sf4-app). Соответственно было бы удобно, чтобы контейнер имел доступ к проекту. Соответственно в volumes мы прописываем ~/projects/my-sf4-app:/app (см. этот пример в docker-compose.yml выше (у меня это указано относительным путем ../:/app)).

Таким образом для контейнера будет расшарена папка и он сможет выполнять в ней различные действия типа php bin/console doctrine:migrations:migrate. Так же эти директории удобно использовать для того, чтобы сохранять данные приложений. Например postgresql можно указать директорию для хранения данных БД и тогда при пересоздании контейнера не нужно будет накатывать дамп или фикстуры.

working_dir — указывается рабочая директория контейнера. В данном случае /app (или по аналогии с вагрантом /var/www/my-sf4-app).

environment — сюда передаются все переменные для контейнера. Например для rabbitmq передаются имя пользователя и пароль, для postgresql передаётся имя базы, имя пользователя, пароль.

container_name — не обязательное поле, однако я предпочитаю указывать, для удобства подсоединения к контейнерам. Если не указать, то будут присвоены имена по дефолту с хэшами.

Это основные параметры, которые необходимо указывать. Остальные могут быть по желанию для дополнительных настроек, либо по документации к контейнеру.

Теперь, чтобы все это запустить необходимо провести команду docker-compose up -d в директории, где расположен файл docker-compose.

Как и где все это хранить для локалки?


Для локалки я использую папку docker в корне проекта.


В ней находится папка data в которой я храню всю информацию postgresql и elasticsearch, чтобы при пересоздании проекта не приходилось накатывать фикстуры с нуля. Так же есть папочка nginx в которой я храню конфиг для локального nginx контейнера. Эти папки я синхронизирую в docker-compose.yml с соответствующими файлами и папками в контейнерах. Так же на мой взгляд очень удобно писать bash скрипты для работы с докером. Например, start.sh скрипт запускает контейнеры, потом проводит composer install, чистит кэш и проводит миграции. Для коллег по проекту это так же удобно, им не приходится что-либо делать, они просто запускают скрипт и все работает.

Пример скрипта start.sh


#!/usr/bin/env bash
green=$(tput setf 2)
toend=$(tput hpa $(tput cols))$(tput cub 6)

echo -n 'Как к вам обращаться?: '
read name
echo "Привет тебе $name! Мы начинаем старт докера для проекта tutmesto.ru"
echo -n "$name, ты хочешь использовать дамп для БД? (y/n): "
read use_dump
echo 'Сейчас мы запустим сборку докера!'
docker-compose up -d || exit
echo -en '\n'
echo -n "Докер успешно собрался! ${green}${toend}[OK]"
echo -en '\n'
echo 'Теперь нам необходимо собрать композер.'
./composer-install.sh
echo -en '\n'
echo -n "Композер успешно собрался ${green}${toend}[OK]"
echo -en '\n'
echo 'Сейчас надо будет заснуть на 40 секунд, чтобы успела развернуться postgres-ка'
sleep 5
echo 'Осталось еще 35 секунд...'
sleep 5
echo 'Осталось еще 30 секунд...'
sleep 5
echo 'Осталось еще 25 секунд...'
sleep 5
echo 'Осталось еще 20 секунд...'
sleep 5
echo 'Осталось еще 15 секунд...'
sleep 5
echo 'Осталось еще 10 секунд...'
sleep 5
echo 'Осталось еще 5 секунд...'
sleep 5
echo 'Сон завершился. По идее postgres-ка уже поднялась и сейчас мы будем закачивать дамп!'

case "$use_dump" in
    y|Y) ./dump.sh
         echo -en '\n'
         echo -n "Дамп успешно закачался! ${green}${toend}[OK]"
         echo -en '\n'
    ;;
    *) echo "$name, хорошо, обойдемся без дампа! =)"
    ;;
esac
echo 'Теперь нам надо провести миграции!'
./migrations-migrate.sh
echo -en '\n'
echo -n "Миграции успешно проведены! ${green}${toend}[OK]"
echo -en '\n'
echo 'Теперь почистим кэш!'
./php-fpm-command.sh rm -rf var/cache/*
./php-fpm-command.sh chmod 777 var/ -R
./cache-clear.sh
echo -en '\n'
echo -n "Кэш успешно очищен! ${green}${toend}[OK]"
echo -en '\n'
echo 'Теперь скопируем настройки для локалки!'
./env.sh
echo -en '\n'
echo -n "Настройки для локалки скопированы! ${green}${toend}[OK]"
echo -en '\n'
echo "Теперь, $name, ты можешь пользоваться локалкой! Открой в браузере localhost:7777 и наслаждайся!"
echo -en '\n'
echo "------------------------------------------------------------------------------"
echo -en '\n'
echo "ОСНОВНЫЕ КОМАНДЫ КОТОРЫЕ МОЖНО ИСПОЛЬЗОВАТЬ:"
echo "./cache-clear.sh                            |Очистка кэша symfony 4"
echo "./composer.sh [command(ex. install)]        |Обращение к композеру"
echo "./composer-install.sh                       |Запуск composer install"
echo "./connect-to-php-fpm.sh                     |Подключение к консоли php"
echo "./console.sh [command(ex. cache:clear)]     |Запуск команды php bin/console"
echo "./destroy.sh                                |Жесткое сворачивание локалки. Убивает все кроме образов."
echo "./dump.sh                                   |Закачать дамп, который находится в корне (dump.sql)"
echo "./env.sh                                    |Скопировать настройки для локалки"
echo "./migrations-migrate.sh                     |Провести миграции"
echo "./php-fpm-command.sh [command(ex. php -m)]  |Выполнить команду в php-fpm контейнере"
echo "./start.sh                                  |Запуск локалки (этот скрипт)"
echo "./stop.sh                                   |Gracefull shutdown локалки"
echo -en '\n'
echo "ДЛЯ УДОБНОГО ПОЛЬЗОВАНИЯ В ДАМПЕ БЫЛИ СОЗДАНЫ СЛЕДУЮЩИЕ ПОЛЬЗОВАТЕЛИ:"
echo "client@c.cc    | QWEasd123"
echo "admin@a.aa     | QWEasd123"
echo "moderator@m.mm | QWEasd123"
echo -en '\n'
echo "------------------------------------------------------------------------------"
echo -en '\n'
echo -en '\n'
echo 'OtezVikentiy brain corporation!'
echo -en '\n'
echo -en '\n'

Пример скрипта php-fpm-command.sh


#!/usr/bin/env bash

cd "`dirname \"$0\"`" &&  docker-compose exec -T "php-fpm" sh -c "cd /app && $*"

Пример скрипта connect-to-php-fpm.sh


#!/usr/bin/env bash
docker exec -i -t --privileged php-fpm bash

Локальная среда разработки на этом заканчивается. Поздравляю, можете поделиться с коллегами готовым результатом! )

Продуктив


Подготовка


Допустим, вы уже что-то написали на локалке и хотите выложить это на production сервер или на тестовый сервер. У вас есть хостинг на KVM виртуализации или свой сервер в соседней комнате с кондиционером.

Чтобы развернуть продуктив или бету — на сервере должна быть операционная система (в идеале linux) и установленный docker. Docker можно установить точно так же, как и на локалку, отличий никаких.

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

Docker для продуктива использует docker swarm и docker stack. Если прям на пальцах, то эта система отличается лишь другими командами и тем, что docker swarm представляет из себя распределитель нагрузок для кластера (опять же немного абстрактно, но так будет проще для понимания).

P.S.: советую потренироваться настраивать docker swarm на Vagrant (как бы ни парадоксально это звучало). Простой рецепт для тренировки — поднимаете пустой Vagrant с той же операционной системой, что и в продуктиве и настраиваете его для начала.

Чтобы настроить docker swarm — необходимо просто выполнить несколько команд:


docker swarm init --advertise-addr 192.168.***.** (ip-адрес вашего сервера)
mkdir /app (в случае если ваш докер настроен на работу с директорией app)
chown docker /app (ну или раздать права на директорию)
docker stack deploy -c docker-compose.yml my-first-sf4-docker-app

Рассмотрим теперь все это немного подробнее.

docker swarm init --advertise-addr — оно запускает непосредственно сам docker swarm и шарит ссылку, чтобы вы могли бы подцепить к этому «рою» еще какой-то другой сервер, чтобы они работали в кластере.
mkdir /app && chown .. — необходимо создать заранее все необходимые директории для работы докера, чтобы во время сборки он не жаловался бы на отсутствие директорий.
docker stack deploy -c docker-compose.yml my-first-sf4-docker-app — эта команда запускает сборку самого вашего приложения, аналог docker-compose up -d только для docker swarm.

Чтобы началась какая-либо сборка, вам необходим все тот же docker-compose.yaml, но уже немного измененный именно под продуктив/бету.

version: '3.1'

services:

  php-fpm:
    image: otezvikentiy/php7.2-fpm:0.0.11
    ports:
      - '9000:9000'
    networks:
      - my-test-network
    depends_on:
      - postgres
      - rabbitmq
    volumes:
      - /app:/app
    working_dir: /app
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == manager]

  nginx:
    image: nginx:1.15.0
    networks:
      - my-test-network
    working_dir: /app
    ports:
      - '80:80'
    depends_on:
      - php-fpm
    volumes:
      - /app:/app
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == manager]

  postgres:
    image: postgres:9.6
    ports:
      - '5432:5432'
    working_dir: /app
    networks:
      - my-test-network
    secrets:
      - postgres_db
      - postgres_user
      - postgres_pass
    environment:
      POSTGRES_DB_FILE: /run/secrets/postgres_db
      POSTGRES_USER_FILE: /run/secrets/postgres_user
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_pass
    volumes:
      - ./data/dump:/app/dump
      - ./data/postgresql:/var/lib/postgresql/data
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == manager]

  rabbitmq:
    image: rabbitmq:3.7.5-management
    networks:
      - my-test-network
    working_dir: /app
    hostname: my-test-sf4-app-rabbit-mq
    volumes:
      - /app:/app
    ports:
      - '5672:5672'
      - '15672:15672'
    secrets:
      - rabbitmq_default_user
      - rabbitmq_default_pass
      - rabbitmq_default_vhost
    environment:
      RABBITMQ_DEFAULT_USER_FILE: /run/secrets/rabbitmq_default_user
      RABBITMQ_DEFAULT_PASS_FILE: /run/secrets/rabbitmq_default_pass
      RABBITMQ_DEFAULT_VHOST_FILE: /run/secrets/rabbitmq_default_vhost
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == manager]

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.3.0
    networks:
      - my-test-network
    depends_on:
      - postgres
    environment:
      - discovery.type=single-node
      - discovery.zen.ping.unicast.hosts=elasticsearch
      - bootstrap.memory_lock=true
      - ES_JAVA_OPTS=-Xms512m -Xmx512m
    ports:
      - 9200:9200
      - 9300:9300
    working_dir: /app
    volumes:
      - /app:/app
      - ./data/elasticsearch:/usr/share/elasticsearch/data
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == manager]

  jenkins:
    image: otezvikentiy/jenkins:0.0.2
    networks:
      - my-test-network
    ports:
      - '8080:8080'
      - '50000:50000'
    volumes:
      - /app:/app
      - ./data/jenkins:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == manager]

volumes:
  elasticsearch:
  postgresql:
  jenkins:

networks:
  my-test-network:

secrets:
  rabbitmq_default_user:
    file: ./secrets/rabbitmq_default_user
  rabbitmq_default_pass:
    file: ./secrets/rabbitmq_default_pass
  rabbitmq_default_vhost:
    file: ./secrets/rabbitmq_default_vhost
  postgres_db:
    file: ./secrets/postgres_db
  postgres_user:
    file: ./secrets/postgres_user
  postgres_pass:
    file: ./secrets/postgres_pass

Как вы можете видеть — файл с настройками для продуктива немного отличается от файла для локалки. В нем добавились secrets, deploy и networks.

secrets — файлы для хранения ключей. Ключи создаются довольно просто. Вы создаете файл с названием ключа — внутрь пишете значение. После этого в docker-compose.yml вы указываете раздел secrets и в него передаете весь список файлов с ключами. Подробнее.
networks — это созадется некая внутренняя сетка, через которую общаются между собой контейнеры. На локалке — это делается автоматически, но на продуктиве — это необходимо немного сделать вручную. Плюс ко всему, можно указывать дополнительные настройки кроме дефолтных. Подробнее.
deploy — это основное отличие локалки от продуктива/беты.

    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == manager]

Минимальный набор бойца:

replicas — указываете количество реплик, которое необходимо запускать (по сути это используется в случае, если у вас кластер и вы используете распределитель нагрузок от докера). Например, у вас есть два сервера и вы их соединили через docker swarm. Указывая здесь цифру 2, например, 1 инстанс у вас будет создан на 1 сервере, а второй на втором сервере. Таким образом, нагрузка на сервера будет разделяться напополам.
restart_policy — политика автоматического «переподнятия» контейнера в случае, если он по какой-то причине упал.
placement — расположение инстанса контейнера. Например бывает случаи, когда вы хотите, чтобы все инстансы контейнера крутились именно на 1 из 5 серверов, а не распределялись между ними.

Хочу почитать документацию!

Итак, мы немного подразобрались с тем, что отличает docker-compose.yaml для локалки от версии для продуктива/беты. Теперь давайте попробуем запустить это дело.

Допустим, вы тренируетесь на Vagrant'е и в корне сервера у вас лежит уже настроенный файл для продуктива docker-compose.yml


sudo apt-get update
sudo apt-get -y upgrade

sudo apt-get install -y language-pack-en-base

export LC_ALL=en_US.UTF-8
export LANGUAGE=en_US.UTF-8
export LANG=en_US.UTF-8

curl -sSl https://get.docker.com/ | sh
sudo usermod -aG docker ubuntu

sudo apt-get install git

sudo docker swarm init --advertise-addr 192.168.128.77

sudo mkdir /app
sudo chmod 777 /app -R

docker stack deploy -c /docker-compose.yml my-app

git clone git@bitbucket.org:JohnDoe/my-app.git /app

docker stack ps my-app
docker stack ls
docker stack services my-app

P.S.: не пинайте за sudo и 777, естественно на продуктиве так делать не стоит. Это лишь для скорости обучения.

Итак, нас более всего интересуют строки связанные с докером.
Сначала мы инициализируем «рой» (docker swarm).
Потом создаем директории, необходимые для работы.
Скачиваем репу с нашим кодом на SF4 в директорию /app.
После этого идут три команды: ps, ls и services.

Каждая из них по-своему полезна. Я чаще всего использую ps, так как она отображает состояние контейнеров и часть ошибки, в случае, если таковая имеется.

Допустим, у нас поднялись контейнеры, но какой-то из них постоянно падает с ошибкой и в docker stack ps my-app вы видите кучу перезапусков. Чтобы увидеть причину падения необходимо выполнить docker container ps -a — и там будет отображаться контейнер, который постоянно падает. Их будет много инстансов одного и того же контейнера, например my-app_php-fpm.1.*какой-либо лютый hash*.

Соответственно, теперь, зная имя контейнера — выполняем docker logs my-app_php-fpm.1.*какой-либо лютый hash* и просматриваем логи. Исправляем ошибку и перезапускаем ВСЁ. Чтобы грохнуть все контейнеры можно сделать так:

docker stack rm my-app

После этого у вас будет чистый swarm без каких-либо контейнеров. Исправляете ошибку — и снова docker stack deploy -c docker-compose.yml my-app.

Комментарии (40)


  1. porn
    20.08.2018 18:06

    Сейчас надо будет заснуть на 40 секунд, чтобы успела развернуться postgres-ка
    Вместо этого для php-fpm можно указать docker-entrypoint файл, в котором проверять доступность постгреса, например:
    until pg_isready --timeout=0 --dbname="${DATABASE_URL}"; do
        sleep 1
    done


    1. mistergonza
      20.08.2018 18:54

      А чем параметр depends_on в docker-compose не подойдёт? Для подобных ситуаций он и нужен.


      1. porn
        20.08.2018 18:57

        depends_on на самом деле не проверяет, готова ли база к использованию:

        depends_on does not wait for db and redis to be “ready” before starting web — only until they have been started. If you need to wait for a service to be ready, see Controlling startup order for more on this problem and strategies for solving it.
        docs.docker.com/compose/compose-file/#depends_on


      1. Zhuravljov
        20.08.2018 19:11

        Он гарантирует, что один контейнер не запустится без другого, и гарантирует последовательность запуска. Но не гарантирует, что к моменту готовности PHP будет готов и контейнер с СУБД. Я для этого в entrypoint использую https://github.com/vishnubob/wait-for-it, и жду пока не откроются нужные порты.


      1. otezvikentiy Автор
        20.08.2018 22:35

        Да, depends_on не проверяет доступность именно самой БД для использования. Контейнер php разворачивается и так в разы быстрее, чем БД. Плюс ко всему БД контейнер уже является доступным, но при этом сама БД еще не работоспособна. Соответственно depends_on воспринимает ее уже как готовую, так как контейнер то типа готов… и происходит тут коллизия… (((


        1. gecube
          21.08.2018 09:03

          Решение простое. В постгрессе делаем healtcheck. А depends_on умеет запускать контейнер, когда другой контейнер дал статус «здоров». Проблема только одна: это синтаксис докер-компоуз второй, а не третьей версии (хотя они и развиваются параллельно)


        1. Zhuravljov
          21.08.2018 18:37

          Контейнер php разворачивается и так в разы быстрее, чем БД

          Это смотря как у вас организован entrypoint для php-контейнеров. У нас в проектах через entrypoint проходит полный цикл инициализации: обновляются composer-пакеты, накатываются миграции БД, и прочее.


    1. otezvikentiy Автор
      20.08.2018 22:33

      Да, я тоже об этом подумал уже потом ))) Но все никак руки не доходят подправить этот момент )))


  1. arteniioleg
    20.08.2018 22:32

    Хочу поделиться как я запускаю IDE внутри Докера на Линуксе.

    1. Устанавливаю x11docker.
    2. Создаю Dockerfile с GUI либами необходимыми для IDE, или использую готовый образ.
    3. В проекте создаю скрипт docker/ide.sh. Там делаю проверки и инициализации, в конце запускаю IDE в контаинере.


    Примеры


    1. otezvikentiy Автор
      20.08.2018 22:33
      +1

      А если не секрет, зачем запускать IDE внутри докера?.. о_О


      1. arteniioleg
        20.08.2018 23:10

        Разработчики ничего не устанавливают, основная система не засорена либами для разработки. Можно работать над проектами которые требуют разные окружения. Все разработчики, вне зависимости от их дистрибутива линукс, работают в одном и том же окружении. IDE имеет доступ к тулзам используемых в проекте: лучше анализирует код, в консоле IDE можно запускать нужные инструменты, из IDE компилируешь и линкуешь с либами установленные в контейнере (C++), запускаешь приложение/сервер/тесты, дебажиш.


        1. gecube
          21.08.2018 09:05

          Хорошее решение. Сами танцевали с бубном вокруг иксов в докере. Единственная серьезная проблема — правильный маппинн пользователя внутри докера наружу. Это критично, когда на одном пк (сервере) работает несколько пользователей, а докер, как известно, может на каталоги внутри volume ставить права, отличные от ожидаемых на хост-машине


        1. eduard_matveev
          22.08.2018 23:32

          Можно просто скопировать из контейнера нужные либы (/usr/include, /usr/lib и тд) в хост машину и прокинуть прокси io на исполняемые файлы через docker exec как например сделал в github.com/eduardmatveev/subsys-proxy


  1. gecube
    21.08.2018 09:08

    Vps? В России разницы между vps и vds нет. Не вводите людей в заблуждение. Вопрос только в технологиях. Т.е. полноценная виртуальная машина — да, докер взлетит. Но у того же Таймвеб были определенные проблемы с бекапом ВМ с докером (особенности qemu agent), которые потом были решены. А другое дело — контейнерная виртуализация (всякие богопротивные openvz): там даже не знаю получиться ли, ХОТЯ ДОКЕР ВНУТРИ ДОКЕРА РАБОТАЕТ!!!!


    1. otezvikentiy Автор
      21.08.2018 20:17

      Да, возможна неточность. Я столкнулся с тем, что, например, на mchost VPS невозможно завести докер. Поэтому решил сообщить об этом людям, чтобы имели в виду, что докер не на любом хостинге взлетит.


  1. n0dwis
    21.08.2018 09:59

    Использую docker недавно, в связи с этим 3 вопроса:

    1. Как корректно выставить права в контейнере? Т.к. id пользователей хоста и контейнера не совпадают, периодически возникает ситуация что с хоста нельзя отредактировать какой-либо файл, либо в контейнере ругается composer и т.п.
    2. Как правильно поместить rsa ключ в контейнер? Secrets, конечно, помогает, но его приходится внутри контейнера копировать в $HOME/.ssh и выставлять права. Это лишние телодвижения, плюс есть опасения, что контейнер может попасть на hub.docker.com
    3. Не могу найти вменяемой инструкции по генерации описаний образов на хабе.


    1. akeinhell
      21.08.2018 10:43

      1. выполнить `chown` для созданных файлов внутри контейнера / создавать файлы не в контейнере
      2. --volume $HOME/.ssh/id_rsa:/root/.ssh/id_rsa:ro


      1. n0dwis
        21.08.2018 11:59

        1. выполнить `chown` для созданных файлов внутри контейнера / создавать файлы не в контейнере

        Это понятно, хотелось бы более изящное решение. У меня пока есть мысль испльзовать группы и групповые права, но до реализации не дошло.

        2. --volume $HOME/.ssh/id_rsa:/root/.ssh/id_rsa:ro

        Не для root-а это не сработает (а может, и для него — не проверял). Права выставляются правильные, но владелец же не меняется.


    1. gecube
      21.08.2018 19:56

      По пункту один — есть опция для отображения id снаружи с хоста в id в контейнере.
      Как-то так. www.jujens.eu/posts/en/2017/Jul/02/docker-userns-remap
      Или так boxboat.com/2017/07/25/fixuid-change-docker-container-uid-gid


      1. n0dwis
        22.08.2018 09:56

        Первый вариант слишком замороченный, а вот второй, похоже, самое оно. Спасибо!


    1. otezvikentiy Автор
      21.08.2018 20:21

      1. Честно говоря сам пока что не до конца решил этот вопрос на проде. На локалке просто 777 выставляю и все. А вот с продом пока что тоже под вопросом.


      2. Я пока что не смог найти лучшего решения, чем расшаривать, на данный момент. На hub.docker.com точно не попадёт


      3. Генерации описаний образов… Ну… Я описание пишу сам в формате как README.md пишется и все.



      1. n0dwis
        22.08.2018 09:57

        А «расшаривать» — что Вы имеете в виду? Монтирование? А как быть, опять же, с владельцем? Поменять можно, но он же поменяется и на хосте.

        Я описание пишу сам в формате как README.md пишется и все.

        А блок с тегами, ссылками на Docker-file? Тоже руками? Плюс там как-то можно это передавать параметром при push-е.


        1. otezvikentiy Автор
          22.08.2018 23:33

          ну да, все руками пишу )))


  1. gietos
    21.08.2018 10:55

    И первая мысль — WTF? Как перебросить это в docker-compose?

    Используйте онлайн конвертер https://composerize.com


  1. gietos
    21.08.2018 11:29

    Либо можно указать 7777:80, и тогда ваш сайт будет по url localhost:7777. Это необходимо бывает для того, чтобы несколько проектов можно было разворачивать на одном и том же хосте.
    Для запуска нескольких проектов на одном хосте очень удобно использовать контейнер jwilder/nginx-proxy. Грубо говоря, это подготовленный контейнер с nginx, который следит за тем, какие контейнеры запущены и автоматически создает виртуальные хосты для тех, у которых в окружении есть переменная VIRTUAL_HOST


    1. gecube
      21.08.2018 19:08

      traefik удобнее. Накидываешь правильные labels на контейнеры, немного магии и ПРОФИТ!!! А самое ценное, что трэфик умеет сам получать сертификаты le


      1. gietos
        21.08.2018 19:15

        traefik удобнее
        чем, например? я не набросить, интересно услышать мнение

        правильные labels на контейнеры, немного магии и ПРОФИТ
        трэфик умеет сам получать сертификаты
        принцип работы аналогичный, jwilder — переменные окружения, у traefik метки. Сертификаты letsencrypt тоже оба решения умеют автоматически получать.

        При этом traefik медленнее (https://docs.traefik.io/v1.5/benchmarks/):
        Traefik is obviously slower than Nginx, but not so much: Traefik can serve 28392 requests/sec and Nginx 33591 requests/sec which gives a ratio of 85%. Not bad for young project :) !


        1. gecube
          21.08.2018 19:54

          jwilder — это не коробочный nginx или nginx plus. Это минус. Жирный.
          Traefik это все умеет из коробки.
          Касательно медленнее — я не понял, что это — жалоба или радость? Потому что такая производительность для нового проекта, причем не для highload очень неплоха.
          А еще у traefik есть изначальная интеграция с прометеус (жирный плюс) и очень симпатичный дашборд (не секьюрен, но можно самому закрыть его от внешнего мира).

          На самом деле попробуйте сами и убедитесь.


  1. mirkhamidov
    21.08.2018 11:46

    надо просто «загуглить» сокращения docker run команды

    мне кажется, что наилучший вариант это (или как «один из» вариантов)
    docker help run
    Быстро, качественно и не нужен интернет.

    PS: Временно оказался в стране где доступ к интернету медленный, порой так быстрее ))


  1. Fian
    21.08.2018 11:47

    Если для локалки

     volumes: - ../:/app
    допустимо, то для прод-окружения тянуть код на машину и прокидывать его в контейнер выглядит чуждо самой концепции докера, плюс добавляет сложность при обновлении, когда недостаточно docker service update, но нужно еще и пулить код с гита. Еще одним недостатком такого является чрезвычайная сложность или даже невозможность быстрого отката (blue green deployment). У себя в проекте для fpm и nginx я наследуюсь от соответствующих образов и клонирую при билде код проекта внутрь образа. Из минусов такого подхода — теряется кеш при каждом релизе, но, по-моему, плюсов от такого подхода больше.

    P.S. У вас не было проблем с производительностью php при использовании докера? Я, конечно, ожидал оверхеда 5-10%, но по факту у меня докер медленнее натива минимум на 30-50% на одинаковых машинах.


    1. otezvikentiy Автор
      21.08.2018 11:48

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


    1. mirkhamidov
      21.08.2018 12:05

      то для прод-окружения тянуть код на машину и прокидывать его в контейнер выглядит чуждо самой концепции докера

      Fian какая правильная концепция?


      1. gietos
        21.08.2018 12:15

        Отвечу про концепцию. Монтирование кода приложения удобно на этапе разработки, чтобы сразу тестировать правки. На продакшене устанавливать гит, стягивать код из репозитория — лишние действия. браз должен содержать в себе готовое к запуску приложение.


      1. tendium
        21.08.2018 15:21

        Мы собираем цельный самодостаточный образ с приложением, который содержит всё, что нужно для работы приложения, и потом используем его в кубернетес. Для разработки, понятное дело, код монтируется.


        1. mirkhamidov
          22.08.2018 09:49

          подход понял. А как на практике прокидываете запросы с хоста (боевого) на уже собранный докер? (Первое что в голову приходит nginx проксирование)


    1. DARAKON
      22.08.2018 23:32

      Flan можно тоже присоеденюсь с вопросом)
      Я правильно понял, вы предлагаете на CI сервере склонить корень проекта в контейнер php и в таком виде запушить его в docker hub?
      А как тогда на продакшен сервере этот же контейнер подтянется и поднимется с остальными? Прописывать поднятие всех 3-4 контейнеров одной командой?
      Сейчас у меня тоже на продакшене нужно тянуть гит, проект, далее из compose файла и volume'ы все хозяйство поднимается. И я согласен с вами что это неправильный путь


      1. Fian
        23.08.2018 11:19

        Да, предложение было в том, чтобы код держать изолированно в контейнере. Дополню, что в моем сообщении смысл был не в том, что тянуть гит-репозиторий на сервер это плохо, а в том, что прокидывать код с диска в контейнер на проде это плохо. Плохо или хорошо использовать гит как инструмент доставки того же docker-compose до серверов — я в этом не спец. У нас все деплой-конфигурации вынесены от кода в отдельный репозиторий, но при деплое они все равно тянутся из гита на сервер (на мастер-ноду кластера) и по ним разворачиваются приложения на слейвах.


  1. mirkhamidov
    21.08.2018 12:04

    У меня тут несколько вопросов возникло (не сочтите за глупость, осваиваем только эту неделю):
    — working_dir — а для каких целей он нужен в docker-compose.yml, в Dockerfile нужен для выполнения команд в это директории (смена директории), насколько знаю?
    — понравилось как упростили работу разработчикам bash-скриптами, можете поделиться?
    — replicas — пока что не совсем понял, что будет если увеличить кол-во на одном сервере?
    — volumes: — в корне файла docker-composer.yml, зачем он? кажется только у вас его видел
    — как же настроить продакшн сервер чтобы уже шла обработка в докере? (у меня конечно получилось это сделать, правда только обработчик php запустил как upstream)
    — и самое главное, как обновлять/откатывать на бою?

    PS: Спасибо за статью, многое прояснилось ))


    1. n0dwis
      22.08.2018 09:55

      • working_dir нужна, по-моему, не для выполнения команд на этапе сборки, а для задания корневого каталога для приложения при запуске контейнера
      • volumes — это для именованных томов, появилась, в docker-compose версии 3.2, если не ошибаюсь


    1. otezvikentiy Автор
      22.08.2018 23:40

      по поводу sh скриптов в личку ответил )))
      replicas — это короче сколько реплик в кластере. Если запустить несколько на одном и том же сервере — ну как бы это тоже имеет плюс в том случае, если один из контейнеров ляжет — то другой возьмет на себя его нагрузку. Но если ляжет сервер, а другого на подхвате нет — то ляжет уже как бы все…

      что значит чтобы шла обработка в докере?

      Обновлять откатывать на бою… я делаю это через jenkins… То есть у меня репа снаружи докера и всем заправляет Jenkins… Jenkins можно настроить версионностью и выгружать не ветки, а теги… например… )))