За последний год программное обеспечение для автоматизации развертывания в среде виртуализации на уровне операционной системы набирает большие обороты. Эта статья послужит новичкам в этой сфере примером, как нужно упаковывать свое приложение в Docker контейнеры.

В классическом виде, PHP приложение представляет из себя следующие составляющие:

  1. Веб-сервер
  2. СУБД
  3. PHP приложение

В нашем примере мы будем использовать Nginx, PostgreSQL и PHP-FPM.

1. Установка Docker


Для начала работы, нам потребуется Docker. Скачать его можно на официальном сайте Docker.

2. Создание образов


Docker создает образы на основе DockerFile файлов, в котором описывается функционал. Мы создадим 3 образа для наших составляющих.

DockerFileNginx


FROM nginx:mainline-alpine

RUN set -ex  && addgroup -g 82 -S www-data  && adduser -u 82 -D -S -G www-data www-data     && mkdir -p /etc/pki/nginx/     && apk update     && apk --no-cache add --update openssl     && openssl dhparam -out /etc/pki/nginx/dhparams.pem 4096     && sed -i -e 's/user\s*nginx;/user  www-data www-data;/g' /etc/nginx/nginx.conf     && sed -i -e 's/worker_processes\s*1;/worker_processes  auto;/g' /etc/nginx/nginx.conf     && rm -rf /var/cache/apk/*

COPY config/website.conf /etc/nginx/conf.d/website.conf

В данном DockerFile мы создаем пользователя www-data с группой 82 и устанавливаем Nginx. Последняя строка COPY предполагает, что у вас конфигурация приложения лежит в папке config/website.conf. Она скопируется в /etc/nginx/conf.d/website.conf.

DockerFilePostgresql


FROM postgres:9.5.2

RUN localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
ENV LANG en_US.utf8

В этом образе, мы будем отталкиваться от образа postgres:9.5.2 и выполним команду для определения локали и языка.

DockerFile


FROM alpine:edge

# Timezone
ENV TIMEZONE Europe/Moscow
ENV PHP_MEMORY_LIMIT 1024M
ENV MAX_UPLOAD 128M
ENV PHP_MAX_FILE_UPLOAD 128
ENV PHP_MAX_POST 128M</blockquote>

RUN set -ex 	&& addgroup -g 82 -S www-data 	&& adduser -u 82 -D -S -G www-data www-data 	&& echo "@testing http://dl-4.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories     && apk update     && apk upgrade     && apk add --update tzdata 	&& cp /usr/share/zoneinfo/${TIMEZONE} /etc/localtime 	&& echo "${TIMEZONE}" > /etc/timezone     && apk --update add --no-cache php7-fpm@testing php7-mcrypt@testing php7-openssl@testing php7-json@testing php7-mysqli@testing php7-session@testing php7-gd@testing php7-xmlreader@testing php7-xmlrpc@testing     php7-zip@testing php7-iconv@testing php7-curl@testing php7-zlib@testing php7@testing php7-ctype@testing php7-pgsql@testing php7-pdo_pgsql@testing bash rsync     && sed -i -e "s/;daemonize\s*=\s*yes/daemonize = no/g" /etc/php7/php-fpm.conf     && sed -i -e "s/listen\s*=\s*127.0.0.1:9000/listen = [::]:9000/g" /etc/php7/php-fpm.d/www.conf     && sed -i -e "s/;chdir\s*=\s*\/var\/www/chdir = \/usr\/src\/app/g" /etc/php7/php-fpm.d/www.conf     && sed -i -e "s/user\s*=\s*nobody/user = www-data/g" /etc/php7/php-fpm.d/www.conf     && sed -i -e "s/group\s*=\s*nobody/group = www-data/g" /etc/php7/php-fpm.d/www.conf     && sed -i -e "s/;clear_env\s*=\s*no/clear_env = no/g" /etc/php7/php-fpm.d/www.conf     && sed -i -e "s/;catch_workers_output\s*=\s*yes/catch_workers_output = yes/g" /etc/php7/php-fpm.d/www.conf     && sed -i "s|;date.timezone =.*|date.timezone = ${TIMEZONE}|" /etc/php7/php.ini     && sed -i "s|memory_limit =.*|memory_limit = ${PHP_MEMORY_LIMIT}|" /etc/php7/php.ini     && sed -i "s|upload_max_filesize =.*|upload_max_filesize = ${MAX_UPLOAD}|" /etc/php7/php.ini     && sed -i "s|max_file_uploads =.*|max_file_uploads = ${PHP_MAX_FILE_UPLOAD}|" /etc/php7/php.ini     && sed -i "s|post_max_size =.*|post_max_size = ${PHP_MAX_POST}|" /etc/php7/php.ini     && sed -i "s/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/" /etc/php7/php.ini     && apk del tzdata     && rm -rf /var/cache/apk/*

COPY . /usr/src/app

RUN chown -R www-data:www-data /usr/src/app

EXPOSE 9000

CMD ["php-fpm7"]

Этот образ послужит нам основным образом для нашего приложения. Сначала мы устанавливаем все необходимое для PHP и PHP-FPM. Далее, мы копируем текущую папку приложения в /usr/src/app, где будет распологаться наше приложение. В самом конце мы запускаем PHP-FPM.

Создание образов на основе DockerFile'ов


И так, у нас есть есть DockerFile'ы, на основе которых мы должны создать образы. Образы создаются очень просто. Достаточно выполнить следующие команды:

docker build -t myusername/myproject-nginx:latest -f DockerfileNginx .

docker build -t myusername/myproject-postgresql:latest -f DockerfilePostgreSql .

docker build -t myusername/myproject:latest .

В дальнейшем советую добавить к этим командам --no-cache, чтобы каждый раз не компилировать составляющие.

Мы создаем образы, прикрепляем их к нашему аккаунту на Docker Hub. Теперь, нам нужно отправить наши образы на репозиторий в Docker Hub. Выполняем следующие команды:

docker push myusername/myproject-nginx:latest

docker push myusername/myproject-postgresql:latest

docker push myusername/myproject:latest

Запуск образов на сервере


Мы почти у цели! Нам осталось загрузить образы из репозитория и запустить их. Загружаем их с помощью следующих команд:

docker pull myusername/myproject-nginx:latest

docker pull myusername/myproject-postgresql

docker pull myusername/myproject

Осталось их запустить. Делается это так же просто.

docker run —name myproject-nginx -d -p 80:80 myusername/myproject-nginx:latest

docker run —name myproject-postgresql9.5.2 -d -p 5432:5432 myusername/myproject-postgresql9.5.2:latest

docker run —name myproject -d -p 9000:9000 myusername/myproject:latest

Вуаля! Наше приложение запущено на Docker контейнерах. И тем не менее, всем читателям-новичкам я бы обязательно ознакомиться с документацией Docker.

Всем желаю успехов в освоении новых технологий!
Поделиться с друзьями
-->

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


  1. kricha
    04.12.2016 17:56
    +3

    Кстати, если вы работаете над несколькими взаимодействующими веб-приложениями, то для связки нужно будет использовать docker-compose, дам мой пример:

    Заголовок спойлера
    version: "2"
    
    networks:
        lan_0:
            driver: bridge
            ipam:
                driver: default
                config:
                    - subnet: 172.19.0.0/24
                      gateway: 172.19.0.21
    
    services:
        nginx:
            build: ./nginx/
            ports:
                - 80:80
                - 443:443
            links:
                - php
            volumes:
                - ./code:/var/www/
            networks:
                lan_0:
                    ipv4_address: 172.19.0.22
        php:
            build: ./php/
            expose:
                - 9000
            volumes:
                - ./code/:/var/www/
            extra_hosts:
                - "website.local:172.19.0.22"
                - "api.local:172.19.0.22"
            networks:
                lan_0:
                    ipv4_address: 172.19.0.23
        mysql:
            build: ./mysql/
            environment:
                MYSQL_ROOT_PASSWORD: root
                MYSQL_DATABASE: dev
            ports:
                - 3306:3306
            volumes:
                - ./mysql/sql:/docker-entrypoint-initdb.d
                - ./mysql/data:/var/lib/mysql
            networks:
                lan_0:
                    ipv4_address: 172.19.0.12
    


    1. Fedcomp
      04.12.2016 18:19

      в docker-compose version 1 все гораздо проще.


      1. ErickSkrauch
        04.12.2016 20:01
        +2

        Указывать явные IP необязательно, docker-compose автоматически перелинкует контейнеры и они по умолчанию будут доступны по имени своего service. В примере выше, вероятно, было важно иметь фиксированные IP.


    1. aig
      04.12.2016 20:11
      +2

      Я бы вообще ничего без docker-compose не запускал, иначе потом черт ногу сломит с этими контейнерами, а compose делает имена вида folder_service_n, делает сам отдельную сетку с таким же названием папки.

      Кстати, а зачем вообще указывать extra_hosts и IP адреса? Docker же позволяет внутри одной сети работать просто по имени сервиса, да и порты все наружу открывать не нужно, достаточно только для nginx.


      1. EmotionTigran
        04.12.2016 21:51

        Docker-compose хороший инструмент и конечно же его стоит использовать. Но все же, я бы исключил его для новичков, которые только-только начинают ознакамливаться с Docker'ом.


        1. hudson
          05.12.2016 22:49

          Ну видимо каждому своё, я наоборот несказанно счастлив, что параллельно учился пользоваться docker и docker-compose.


      1. hudson
        05.12.2016 22:46

        имена вида folder_service_n

        Я даже больше скажу, я никогда не использую чистый образ (типа image: mysql:latest), а делаю примерно так:

        version: '2'
        services:
            myproject-db:
                build:
                    context: .
                    dockerfile: .docker/dev/db/Dockerfile
                image: myproject:db
                container_name: myproject_db
        


        Таким образом я получаю фиксированные имена образа и контейнера (если использовать версионирование, то имя образа лучше писать в виде myproject-app:vesion, как принято в dockerhub).

        При этом Dockerfile может содержать просто FROM mysql:latest, или же дополнительные инструкции.


    1. amberovsky
      05.12.2016 15:13
      -1

      Зачем в данном примере явно создавать сеть и прописывать ip?
      docker-compose всё сделает за вас


      1. kricha
        05.12.2016 18:49

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


        1. amberovsky
          05.12.2016 18:54

          Что вы имеете ввиду? Используя docker-compose можно обращаться к «своим» хостам по имени сервиса


          1. kricha
            05.12.2016 18:57

            Обьясняю. У вас есть nginx-контейнер и php-контейнер, они оба дают возможность работе двум web-сервисам site.local и api.local, если из nginx-контейнера вы можете получить доступ к обоим хостам, то в php-контейнере вам ни один не будет доступен, и из кода site.local нельзя будет сделать обращение к api.local.


            1. amberovsky
              05.12.2016 21:58

              Я понял, вам нужны алиасы на nginx сервис.
              Вы пробовали networks — aliases?


  1. kiaplayer
    04.12.2016 18:30

    Можете мне пояснить, как человеку незнакомому с Docker: если ли смысл использовать описанный подход вместо связки Vagrant + VirtualBox?


    1. kricha
      04.12.2016 18:34
      +1

      вагрант — очень тугой инструмент, докер же быстрее, да и удобнее, как по мне.


    1. A1MaZ
      04.12.2016 19:43

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


    1. ppa
      04.12.2016 22:29

      Все от задач зависит. Лично я использую докер в нескольких виртуалках, развернутых варгантом (к слову, вагрант не показался мне удачным решением для разворачивания и уж тем более управления большим количеством виртуалок). Опять же, если требуется запустить зоопарк сервисов на одной машине, то докер отличное решение. Если же нужно давать доступ к машине 3-м лицам, то однозначно виртуалки.


    1. renskiy
      05.12.2016 05:29
      +1

      Если ваша связка Vagrant + Virtualbox работает стабильно, и у вас нет необходимости периодически добавлять новые сервисы в проект, то данную связку вполне удобно использовать, особенно если у вас уже все настроено и отлажено, включая процессы.

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

      Я как раз имел такой опыт перехода с Vagrant на Docker. И при этом столкнулся с рядом трудностей. Например, Docker из коробки не предлагает никакой автоматизации. Есть конечно docker-compose, но его возможности по сравнению с Vagrant весьма скудные (YAML все-таки не сравнить с полноценным ЯП). В итоге пришлось разрабатывать свое собственное решение.

      Если вы все же захотите попробовать Docker без отказа от Vagrant, то последний предоставляет такую возможность, но разобраться в настройке не так то просто. Мне очень помогло в свое время вот это описание процесса настройки Vagrant + Docker.


    1. BAG_Art
      05.12.2016 15:13

      https://github.com/laradock/laradock
      этот проект реализует подход vagtant + homestead
      там же можно полистать docker-compose.yml


  1. kesn
    04.12.2016 23:43
    +9

    Куча sed'ов выглядит ужасно. Я бы просто создал конфиг-файл и переписал бы им дефолтный файл конфига


    1. ls1
      05.12.2016 06:53
      -2

      Куча sed'ов выглядит ужасно.
      Альтернативы ни чем не лучше, везде будет тот же самый PCRE (в лучшем случае)
      Я бы просто создал конфиг-файл и переписал бы им дефолтный файл конфига
      Простые решения увы не всегда подходят, иногда надо сохранить имеющийся конфиг. А со временем приходит понимание, что sed вовсе не ужасен, а совсем даже наоборот


      1. OnYourLips
        05.12.2016 08:58
        +3

        Альтернативы ни чем не лучше, везде будет тот же самый PCRE (в лучшем случае)

        Гораздо лучше: допишите эти измененные строки в новом ini-файле в mods-available директории.
        Не надо править конфиги: либо переписываете своим, либо дополняете (самый чистый вариант).

        А со временем приходит понимание, что sed вовсе не ужасен, а совсем даже наоборот

        Он не просто ужасен, а отвратителен, и уровень отвратительности пропорционален величине проекта.
        Например, вариант с шаблонизатором (подход популярен в средствах управления инфраструктурой) гораздо чище.


        1. ls1
          05.12.2016 10:43

          В данном случае вместо sed что именно предлагаете?


          1. darken99
            12.12.2016 15:56

            Dockerize


        1. KlimovDm
          05.12.2016 11:11

          Он не просто ужасен, а отвратителен, и уровень отвратительности пропорционален величине проекта.
          Не могли бы вы более подробно раскрыть свою мысль?


          1. foxmuldercp
            05.12.2016 13:04
            +1

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


            Я например, не один раз в работе получал в наследство такие вот "однострочники", в которых сам автор через полгода после написания разобраться не мог :(


            В этом плане мне нравятся шаблонизаторы, вроде встроенного в ruby ERB, с помощью которого можно генерить вполне приятные вещи, и не только конфиги


  1. nskforward
    05.12.2016 13:54
    +1

    А почему вы для PHP не используете готовый официальный образ?


    1. nskforward
      05.12.2016 14:03

      Также не вижу в PHP образе перенаправление логов в потоки вывода. Вы как логи с контейнера считываете?


      1. EmotionTigran
        05.12.2016 15:12

        Это форк официального образа для PHP. Для моего проекта там немного по-другому настраивается PHP, в этом же случае, оставил все по дефолту, но показал людям настройки, чтобы было понятно. На счет логов, действительно их нет. Не учел, добавлю как буду у компьютера. Спасибо!


  1. vopper
    05.12.2016 15:13

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


  1. jok40
    05.12.2016 15:28

    Эта статья послужит новичкам в этой сфере примером, как нужно упаковывать свое приложение в Docker контейнеры.
    Я — новичёк в этой сфере и раздел DockerFile из этой статьи выглядит для меня также, как нарисовать_сову.jpg


    1. EmotionTigran
      05.12.2016 15:47

      В статье написано «Docker создает образы на основе DockerFile файлов, в котором описывается функционал. Мы создадим 3 образа для наших составляющих.».


      1. jok40
        05.12.2016 17:29
        +2

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

        PS: Как_нарисовать_сову.jpg
        image


  1. pluseg
    05.12.2016 16:50

    Я правильно понимаю, что после pull'a контейнеров и их запуска, нам еще нужно запустить миграции базы?


    1. EmotionTigran
      05.12.2016 17:21
      -1

      Запуск миграций можно написать в конце DockerFile'а в команде CMD.
      CMD(«php bin/console doctrine:migrations run»);


      1. hudson
        05.12.2016 22:36

        При сборке контейнера контейнер базы данных не обязательно будет запущен. Я пока этот момент путём не решил. Ниже есть комментарий про использование /docker-entrypoint-initdb.d, но это не совсем миграции. Хотя дамп туда можно загрузить, но выполняться он будет только при первом запуске контейнера сразу после его создания.


    1. nskforward
      05.12.2016 17:52

      При создании контейнера из образа, вы можете поместить любое количество файлов с расширением .sql в специальную папку (смотреть в документации к образу вашей БД), которые автоматически исполнятся


  1. amberovsky
    05.12.2016 18:55

    Я вам предложу вместо длинной портянки chain-вызовов — сделать несколько .sh-файлов.

    И я думаю не стоит делать -p на posgresql/project порты, так как они не должны быть доступны снаружи (как минимум сделать ограничение на 127.0.0.1). У вас есть expose в project, всё, что осталось — использовать link при создании контейнера


  1. ALexhha
    07.12.2016 02:34
    +2

    В дальнейшем советую добавить к этим командам --no-cache, чтобы каждый раз не компилировать составляющие

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


  1. Dominant
    07.12.2016 11:07

    Может пропустил, каков процесс деплоя с докером без доунтайма?


    1. nskforward
      08.12.2016 15:20

      Лично я только 2 способа знаю (если кто знает ещё, поделитесь):
      1) Самый простой. Вы загружаете новый образ своего приложения в docker-репозиторий. Затем на сервере в папке вашего приложения выполняете «docker-compose up -d». После чего докер сперва скачает новый образ, грохнет старый контейнер и создаст новый. В этот момент я без остановки делаю рефреш страницы в браузере. При очередном рефреше страницы вижу уже новую версию в браузере, никаких ошибок или зависания не обнаружил.
      2) Более сложный. Нужно использовать прокси с несколькими бэкендами. Поднимаете новый бэкенд с новой версией приложения, если всё ок, тушите бэкенд со старой версией приложения. В этом случае можно протестировать работу на небольшом количестве пользователей как ведёт себя новая версия прежде чем полностью переключаться.


      1. nskforward
        08.12.2016 15:51

        В первом варианте сперва нужно сделать docker pull образа перед вызовом «docker-compose up -d»


  1. letchik
    07.12.2016 11:07

    А как вы статику обслуживаете? Через php? Я не вижу приложения в nginx контейнере.


    1. EmotionTigran
      07.12.2016 11:15

      Статика у меня лежит в volumes, который подключается к nginx контейнеру. Думаю, следующая статья затронет больше тонкостей.