Деплой проекта на php/python с помощью Docker. Часть 1.

Введение

В этой статье я покажу пример "контейнеризации" backend'а веб-приложения, написанного на php/python, MongoDB и Redis с помощью Docker'а (ru.wikipedia.org/wiki/Docker). Этот пример отнюдь не претендует на звание "best practice", а просто показывает, как можно упаковать backend в контейнеры для быстрого и удобного развёртывания на любой linux-платформе, будь то продакшн-сервер или ноутбук разработчика.

Сам пример - вполне реальный, представляет собой серверную часть мобильного приложения-мессенджера. Часть проекта написана на PHP+Yii (админка, авторизация, JSON API), часть - на python+tornado (собственно, сам обмен сообщениями между клиентами). В качестве основной БД используется MongoDB. Redis используется для хранения временных данных, а также - для передачи событий из php-скриптов в python-сервер с помощью Redis PUBSUB.

Почему Docker?

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

Способ доставки приложения на сервер

Доставлять приложения в Docker-контейнерах на продакшн-сервер можно по-разному. Можно локально собирать контейнер с приложением и отправлять его (docker push) в приватный докер-репозиторий, а потом на продакшн-сервере забирать(docker pull) из репозитория, аналогично работе с GIT/Github. Также Docker поддерживает интеграцию с GIT, автоматическую сборку и доставку на продакшн по триггерам (docs.docker.com/docker-hub/builds/). Мной был выбран другой, на мой взгляд, более простой вариант - приложение доставляется на сервер через GIT-репозиторий и собирается в контейнер уже там.

Контейнеры БД

В первую очередь необходимо запустить Mongo и Redis. В этом примере они запускаются на том же сервере, что и само приложение.

#! /bin/bash
sudo mkdir -p /var/data/db
sudo docker stop mongo
sudo docker rm mongo
sudo docker run --name mongo -d -p 27017:27017 -v /var/data/db:/data/db mongo:2.4
sudo docker stop redis
sudo docker rm redis
sudo docker run --name redis -d -p 6379:6379 -v /var/data:/data redis

Скрипт останавливает и удаляет (если есть) существующие контейнеры и создаёт новые. Контейнеры запускаются таким образом, чтобы они работали на стандартных портах, это позволит нам работать с БД с помощью консольных утилит (mongo, redis-cli) без дополнительных параметров, как если бы mongo и redis были установлены непосредственно на хост-сервер.

С помощью параметра "-v" монтируем директорию с данными бд в /var/data на хост-машине, это позволяет нам останавливать/перезапускать/удалять контейнеры без потери данных.
Параметр "--name" задаёт имя контейнера, его мы будем использовать для того, чтобы "связать" контейнеры бд с контейнерами приложений.

Контейнер php-сервера

PHP-сервер написан на Yii, запускается с помощью nginx + php-fpm. Если делать так, как советуют инженеры Docker'а в best practices, следовало бы выделить отдельный контейнер для nginx, отдельный - для php-fpm, и volume-контейнер для статики, и всё это особым образом слинковать между собой. Я решил, что, на данном этапе это перебор, и проще упаковать всё вместе в один контейнер. Итак, Dockerfile контейнера:

FROM debian:jessie

# nginx installation and common settings
RUN apt-key adv --keyserver pgp.mit.edu --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
RUN echo "deb http://nginx.org/packages/mainline/debian/ wheezy nginx" >> /etc/apt/sources.list

ENV NGINX_VERSION 1.7.8-1~wheezy

RUN apt-get update && apt-get install -y nginx=${NGINX_VERSION} && rm -rf /var/lib/apt/lists/*

# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log
RUN ln -sf /dev/stderr /var/log/nginx/error.log

# php installation
RUN apt-get update && \
    apt-get install -y \
    php5-fpm \
    php5-cli \
    php5-curl \
    php5-gd \
    php5-redis \
    php5-mongo \
    && rm -rf /var/lib/apt/lists/*

# nginx settings
RUN mkdir -p /var/www/log
VOLUME /var/www/log

COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./nginx/app /etc/nginx/sites-enabled/app

EXPOSE 80

# php settings
COPY ./php/php.ini /etc/php5/fpm/php.ini
COPY ./php/php-fpm.conf /etc/php5/fpm/php-fpm.conf
COPY ./php/www.conf /etc/php5/fpm/pool.d/www.conf

# app settings
RUN mkdir -p /var/www/app && mkdir -p /var/www/yii
WORKDIR /var/www/app
ADD ./yii.tar /var/www/

CMD service nginx start &&\
    service php5-fpm start &&\
    /bin/bash

Сначала устанавливается nginx, процесс установки позаимствован из официального Dockerfile. Далее устанавливается php со всеми необходимыми расширениями.

Настраиваем логи nginx. В скрипте запуска сделаем так, чтобы они были доступны на хост-сервере.

# nginx settings
RUN mkdir -p /var/www/app/log
VOLUME /var/www/app/log

Далее - копируем в контейнер файлы настроек nginx и php, которые лежат рядом с нашим Dockerfile. Тут я вижу ещё один плюс - файлы настроек(например php.ini) лежат в репозитории, и чтобы что-то в этих настройках поменять, нужно отредактировать необходимый файл и пересобрать контейнер.

В /var/www/app будет лежать, собственно, сам проект. Т.к. в проекте используется Yii Framework, нам нужно скопировать исходники yii в /var/www/yii:

ADD ./yii.tar /var/www/

- распаковывает и копирует архив с исходниками. Вместо этого можно было бы взять их из github-репозитория, но для этого надо было бы устанавливать в контейнер git.

Ну и, наконец, запускаем всё необходимое:

CMD service nginx start &&\
    service php5-fpm start &&\
    /bin/bash

Помимо nginx и php-fpm запускаем /bin/bash, чтобы можно было подключиться к работающему контейнеру.

Итак, у нас есть Dockerfile для контейнера с nginx и php. Теперь перейдём к запуску.

Запуск контейнера php-сервера

Сразу приведу листинг скрипта запуска:

#! /bin/bash
sudo mkdir -p /var/log/app/nginx
sudo docker build -t example/app .
sudo docker stop app
sudo docker rm app
sudo docker run -d -i -t \
    --name app \
    --link mongo:mongo \
    --link redis:redis \
    -p 80:80 \
    -h app \
    -v /var/log/app/nginx:/var/www/log \
    -v $(pwd):/var/www/app \
    example/app

Команда docker build создаёт контейнер из нашего Dockerfile, далее мы пытаемся остановить и удалить запущенный контейнер с приложением, чтобы не делать это вручную каждый раз при пересборке (если контейнер app уже запущен, новый с таким же именем запустить нельзя).

Ну и самое инересное - запуск контейнера. Называем его app (--name), связываем с контейнерами баз данных (--link), таким образом, mongodb и redis будут доступны внутри контейнера как mongo и redis соответственно. То есть, в настройках Yii мы можем, например, указать:

    'mongodb' => array(
            'class'            => 'EMongoDB',
            'connectionString' => 'mongodb://mongo',
            'dbName'           => 'app',
        ),

Прокидываем 80 порт наружу: -p 80:80, потом монтируем директорию контейнера с логами nginx (/var/www/log) в директорию хоста (/var/log/app/nginx), чтобы смотреть логи с локальной машины (ну и чтобы они не исчезали при перезапуске контейнера).

То же самое делаем для самого главного - нашего кода. Монтируем /var/www/app контейнера в текущую директорию хоста $(pwd), то есть туда, где лежит код (скрипт должен запускаться из этой директории, bash я не знаю, поэтому лучшего решения не придумал :) ). Это позволит нам работать с проектом так, будто он запущен не в контейнере, а с помощью установленных на хост-машину nginx и php-fpm, то есть, например, обновлять код с помощью обычного git pull в папке проекта без пересборки контейнера.

На этом пока всё, в следующей части расскажу про сборку контейнера для python-приложения и о том, как с этим всем работать.