2017-11-26 23:01:00 Dev

Docker + Nginx + php7 + mysql = ♥

Docker

Сегодня пойдет речь о такой всеми любимой и обожаемой технологии, как Docker. А подопытные для заворачивания в контейнеры не менее любимые и обожаемые: PHP,  MySQL, NGINX. Говоря по русски - соберем LEMP окружение в котором за пару кликов можно будет развернуть php приложение.

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

Disclimer

Статья предполагает что читатель до этого слышал что такое докер, и понимает зачем он ему нужен. Так же предполагается что читатель достаточно владеет linux что-бы выполнять в нем простейшие команды.

Первоначальная настройка

Для работы, прежде всего нам понадобится сам Docker. Для этого идем на официальный сайт скачиваем и устанавливаем докер для своей платформы. Нас интересует Docker Community Edition (CE) (существует так же версия за деньги Docker Enterprise Edition (EE)).

Также нам понадобится инструмент docker-compose. При установке Docker на Mac у нас сразу устанавливается docker toolbox, в котором есть необходимый нам инструмент docker-compose.

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

$ docker -v
Docker version 17.06.0-ce, build 02c1d87

$ docker-compose -v
docker-compose version 1.14.0, build c7bdf9e

Структура проекта

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

| .database
| code
| containers
|  |_ nginx
|      |_ conf
|  |_ php
|      |_ conf
| logs
| docker-compose.yml

Опишем что где лежит:

  • .database - директория с дампом базы данных (далее станет понятно зачем).
  • code - директория с основным кодом приложения
  • containers  - директория с файлами конфигурации для контейнеров. Соответственно одна для контейнера с Nginx, вторая - для php.
  • logs - директория для access.log и error.log из nginx
  • docker-compose.yml - файл docker-compose с помощью которого и будет разворачиваться приложение.
  • Конфигурация контейнеров

Nginx

Конфигурация контейнеров и их взаимодействие описывается в файле docker-compose.yml.

Для начала определимся с сервером. Нам понадобится nginx последней версии. Добавим в наш docker-compose.yml следующие строки:

nginx:
  image: nginx:latest
  ports:
    - "8081:80"
  volumes:
    - ./code:/code
    - ./containers/nginx/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf
    - ./logs:/var/log/nginx

В первой строке указываем имя контейнера - nginx (оно может быть любым). Далее, во второй - указываем image на основе которого будет собран контейнер. nginx:1.13.1 означает что мы берем за основу официальный образ nginx версии 1.13.1.

Инструкция ports определяет на каком порту будет доступен контейнер, при этом слева от двоеточия мы определяем внешний порт по которому будет доступно наше приложение,  а справа - внутренний - порт через который контейнеры будут общаться друг с другом по внутренней сети docker’a. Для разнообразия поднимим приложение на 81 порту.

В инструкции volumes перечислены точки монтирования. Принцип здесь аналогичный: слева от двоеточия - путь до каталога на хостовой машине, справа от двоеточия - путь до каталога внутри контейнера.

  • ./code:/code  - берем из корня проекта папку code и монтируем в корень файловой системы  контейнера.
  • ./containers/nginx/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf Аналогично монтируем файл конфигурации сервера в директорию внутри контейнера /etc/nginx/conf.d/nginx.conf
  • ./logs:/var/log/nginx - Точно так же монтируем директорию для хранения логов.

Теперь добавим файл конфигурации nginx в директорию /containers/nginx/conf/nginx.conf

server {
	index index.php;
	server_name 127.0.0.1;
	error_log  /var/log/nginx/error.log;
	access_log /var/log/nginx/access.log;
	root /code;

	location / {
		try_files $uri $uri/ /index.php?q=$uri&$args;
	}
	
	location ~ \.php$ {
		fastcgi_split_path_info ^(.+\.php)(/.+)$;
		fastcgi_pass php:9000;
		fastcgi_index index.php;
		include fastcgi_params;
		fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
		fastcgi_param PATH_INFO $fastcgi_path_info;
	}
}

Конфигурация самая стандартная для php - есть на сайте nginx. Единственное что требует пояснений это строка

fastcgi_pass php:9000;

Дело в том, что в пределах внутренней сети docker’a к контейнерам можно обратиться не по его ip адресу, по его имени, что мы и делаем в этой строке.

PHP

Теперь время контейнера с php. Добавим в docker-compose.yml эти строки:

php:
  image: php:7-fpm
  volumes:
    - ./code:/code
    - ./containers/php/conf/log.conf:/usr/local/etc/php-fpm.d/zz-log.conf

Мы создаем контейнер с именем  php на основе образа php:7-fpm а так же монтируем два тома - один с кодом, второй с конфигурацией php для логирования ошибок в коде php скриптов.

Содержимое файла ./containers/php/conf/log.conf

php_admin_flag[log_errors] = on

php_flag[display_errors] = off

Любопытный читатель мог заметить префикс zz- в имени файла справа от двоеточия - этот префикс означает что содержимое файла ./containers/php/conf/log.conf нужно загружать в последнюю очередь, чтобы эта конфигурация не была переопределена другими конфигурациями.

Уже сейчас наше приложение может исполнять php файлы.

Чтобы проверить что все работает, добавим в папку code файл index.php c таким содержимым:

<?php
phpinfo();

Теперь все готово для первого теста. Открываем терминал и переходим в корень проекта. Далее выполняем:

docker-compose up

Если все сделано правильно, в терминале будет примерно такой вывод:Теперь заходим в браузере по адресу 127.0.0.1:8081 и видим стандартную страницу вывода phpinfo()!

Контейнер nginx принял запрос и запустил контейнер с интерпретатором php который выполнил index.php. Все просто, и самое главное - здесь нет никакой магии, хотя со стороны может так не показаться. Однако одна хитрость здесь все же есть, а именно то, каким образом контейнеры вдруг смогли получить доступ друг к другу? Для понимания этого достаточно посмотреть в начало вывода команды docker-compose up:

Docker-compose автоматически создал дефолтную сеть и подключил туда все контейнеры из docker-compose.yml (подробнее о сетях в docker).  Можно даже пингануть один контейнер из другого:

docker exec -ti lemp_php_1 ping lemp_db_1

MySQL

Остается только добавить контейнер с базой данных. Здесь все просто, но есть одна хитрость. Вернее две.

Первая заключается в эфемерной природе файловой системы докер-контейнеров. Дело в том, что изначально докер-контейнеры проектировались для stateless приложений. Создав такой контейнер и поработав в нем, его легко можно убить и никаких следов его существования не останется. От сюда следует, что все данные и измененные файлы умрут вместе с контейнером.

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

Но для начала добавим в docker-compose.yml контейнер с MySQL:

db:
  image: mysql:8
  volumes:
    - ./.database:/var/lib/mysql
  environment:
    MYSQL_ROOT_PASSWORD: example

Здесь мы берем за основу образ mysql 8 и создаем контейнер с именем db. Почему 8 а не 5,7? Судя по всему в версии контейнера 5.7 есть баг, который не дает корректно примонтировать директорию /var/lib/mysql к хостовой машине.

Инструкцией volumes мы создаем точку монтирования для того чтобы сохранить наши данные при перезапуске контейнера. Синтаксис аналогичный: слева от двоеточия путь на хостовой машине, справа - путь внутри контейнера. Инструкцией environment мы задаем рутовый пароль для доступа к БД.

Теперь при запуске mysql в корне проекта появится директория .database в которой будет хранится вся структура БД.

Для доступа к БД из кода достаточно обратиться на URI mysql://db:5432. Подробнее можно почитать здесь