Здрасте!
В последнее время все чаще задумываюсь об оптимальности рабочего процесса и хотелось бы поделиться своими изысканиями в данном вопросе.
В данном посте поговорим про docker-compose, который по моему мнению является панацеей в вопросе организации и оптимизации рабочего процесса разработчика.
Описывать все я буду почти на пальцах, поэтому если вы до этого ни разу не слышали про docker (что странно), ни разу с ним не работали и хотите в нем разобраться, то прошу под кат.
Предисловие
В статье я умышленно упрощаю некоторые моменты, не углубляюсь в детали и поверхностно касаюсь многих вопросов.
Делаю я это с полным понимаем своих действий, и считаю, что не нужно лазить под капот, если все работает.
Те, кто считают иначе — это ваше полное право, но только не нужно навязывать свою точку зрения.
Спасибо!
Docker
Технология, позволяющая легко (во всех смыслах) разворачивать необходимое рабочее окружение.
Подробнее можно узнать из ссылок в конце статьи, но лучше сейчас на это не переключаться, чтобы не забивать себе мозг.
Docker-compose
Пакетный менеджер (по аналогии с composer и npm, только у docker — контейнеры), позволяющий описывать необходимую структуру в одном файле (конфиге).
Подробнее также можно узнать из ссылок в конце статьи.
Docker-hub
Репозиторий контейнеров (по аналогии с packagist и npm).
Важное замечание: внимательно читайте описание к образу, 70-80% тупых вопросов там описано, не тратьте время на гугление.
Установка
Переписывать документацию docker я не стану, поэтому просто кину ссылки:
Установка обычного программного обеспечения (ПО), проблем возникнут не должно.
Если все же возникнут, то дальше можете не читать, вероятно вы случайно наткнулись на эту статью и разработку в целом...
Если вы устанавливали docker под Windows, то работать нужно через специальную консоль Docker Quickstart Terminal. После установки, на рабочем столе должен появиться соответствующий ярлык.
Структура проекта
Для начала определимся со структурой проектов:
- project 1
- project 2
- project N
- src
- container 1
- container 2
- container N
- docker-compose.yml
Каждый проект в обязательном порядке имеет docker-compose.yml и директорию src.
Также для каждого контейнера должна быть своя директория (совпадающая с названием контейнера), где будет храниться вся информация, необходимая для работы контейнера (конфиги, данные, ...).
CMD / Terminal
Для работы с docker и compose мы будем использовать всего несколько команд:
- docker ps – просмотр всех контейнеров (подробнее),
- docker-compose up --build – сборка проекта. Параметр build используется для того, чтобы заставить compose пересоздавать контейнеры. (подробнее).
Описание прочих команд, можно найти на официальном сайте.
Перейдем непосредственно к делу.
apache
https://hub.docker.com/_/httpd/
Начнем, пожалуй, с самого популярного сервера — Apache.
Создадим директорию проекта:
- project
- src
- docker-compose.yml
Конфиг будет выглядеть таким образом:
version: '3'
services:
apache:
image: httpd:2.4
ports:
- 80:80
volumes:
- ./src:/usr/local/apache2/htdocs
Что здесь происходит:
image: httpd:2.4
— указываем какой образ нам нужно и его версию (список доступных версий и модификаций можно посмотреть в соответствующем docker-hub).ports: 80:80
— пробрасываем порты между docker и нашей машиной, т.е. все запросы которые будут идти на 80 порт нашей машины, будут транслироваться на 80 порт docker.volumes: ./src:/usr/local/apache2/htdocs
— линкуем директорию на нашей машине, с рабочей директорий apache, т.е. все файлы находящиеся в директории src, будут доступны для apache, как будто они лежат в директории htdocs (обратное тоже работает, все что создано в docker "копируется" на локальную машину).
Создадим файл src/index.html в рабочей директории с содержимым:
Hi, I'am Apache
Запускаем наш проект:
docker-compose up --build
Переходим в браузер по адресу ПК и наблюдаем приветствие нашего сервера.
Чтобы уничтожить контейнеры нашего проекта, достаточно в консоле выполнить Ctrl+C.
Если докер работает через VirtualBox, то нужно заходить по IP виртуалки. При любом раскладе, если вы работаете через Docker Quickstart Terminal, то адрес выведется при запуске консоли.
Работа в фоновом режиме
Если вам необходимо запустить docker и далее работать в консоле, то можно запустить проект в фоном режиме:
docker-compose up --build -d
После запуска, консоль будет доступна для работы.
Чтобы в данной ситуации уничтожить контейнер, необходимо его остановить и удалить, но для начала, нужно узнать его ID:
docker ps
В моем случае получился такой вывод:
CONTAINER ID IMAGE ...
988e27da7bdf httpd:2.4 ...
Теперь останавливаем и удаляем наш контейнер:
docker stop 988e27da7bdf
docker rm 988e27da7bdf
Либо можно поступить грубо и сразу удалить:
docker rm -f 988e27da7bdf
nginx
https://hub.docker.com/_/nginx/
Конфиг nginx строиться по той же самой схеме, что и apache: образ, порты, рабочая директория. Выглядит файл таким образом:
version: '3'
services:
nginx:
image: nginx:1.13
ports:
- 80:80
volumes:
- ./src:/usr/share/nginx/html
Создаем файл src/index.html в рабочей директории с содержимым:
Hi, I'am Nginx
Заходим в браузер, и видим приветствие очередного сервера.
php + apache
Если говорить про связку PHP и Apache, то для нее уже есть готовый образ, поэтому про линковку контейнеров будем говорить далее. А сейчас просто конфиг:
version: '3'
services:
web:
image: php:7.2-apache
ports:
- 80:80
volumes:
- ./src:/var/www/html
Создаем файл src/index.php в рабочей директории с содержимым:
<?php
phpinfo();
Проверяем его работу в браузере.
php + nginx
https://hub.docker.com/_/nginx/
https://hub.docker.com/_/php/
В данной связке php будет в формате fpm. Схематично это выглядит таким образом:
Соответственно нам нужно будет переписать конфиг сервера.
Для этого нужно будет слинковать помимо рабочей директории, еще и файл конфигурации сервера:
version: '3'
services:
nginx:
image: nginx:1.13
ports:
- 80:80
volumes:
- ./src:/usr/share/nginx/html
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- php
php:
image: php:7.2-fpm
volumes:
- ./src:/usr/share/nginx/html
Что изменилось:
volumes: ./nginx/nginx.conf:/etc/nginx/nginx.conf
— линкуем файл конфига nginx;depends_on: php
— указываем зависимость nginx от php, т.е. по сути, это гарантия того что контейнер php запуститься раньше nginx.
Если мы не укажем depends_on, то можем словить подобную ошибку:
nginx_1 | 2018/01/05 08:56:42 [emerg] 1#1: host not found in upstream "php" in /etc/nginx/nginx.conf:23
nginx_1 | nginx: [emerg] host not found in upstream "php" in /etc/nginx/nginx.conf:23
Создаем файл /nginx/nginx.conf в директории нашего проекта. Сам конфиг выглядит таким образом:
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
root /usr/share/nginx/html;
listen 80;
server_name localhost;
location / {
index index.html index.htm;
}
location ~ \.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;
}
}
}
Все стандартное, главное, чтобы директива root совпадала с docker-compose.yml.
Глядя на конфиг, прошу обратить внимание на директиву fastcgi_pass, а именно на значение php:9000
.
Обычно, когда настраивается локальный сервер указывается localhost:9000
, НО т.к. php у нас находится в другом контейнере, то обращаться мы должны именно к нему (docker сам "подставит" нужный IP контейнера, по сути вся магия скрыта в простом дописывании файла hosts).
Чтобы это стало возможным, в наш docker-compose нужно добавить директиву links (хотя по факту не надо, подробнее).
После всех действий, директория нашего проекта выглядит таким образом:
- project
- src
- index.php
- nginx
- nginx.conf
- docker-compose.yml
- src
Запускаем, проверяем, радуемся!
php + apache + nginx
https://hub.docker.com/_/nginx/
https://hub.docker.com/_/php/
https://hub.docker.com/_/httpd/
Наверное, самая популярная связка для веб-проектов. Схематично это выглядит так:
Пара замечаний:
- php используется как php-fpm, потому что быстрее и моднее;
- apache используется, потому что популярен и htaccess.
Для того чтобы все настроить нам нужно будет также слинковать конфиг apache, и таким образом docker-compose будет выглядеть так:
version: '3'
services:
apache:
image: httpd:2.4
volumes:
- ./src:/var/www/html
- ./httpd/httpd.conf:/usr/local/apache2/conf/httpd.conf
depends_on:
- php
nginx:
image: nginx:1.13
ports:
- 80:80
volumes:
- ./src:/var/www/html
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- apache
php:
image: php:7.2-fpm
volumes:
- ./src:/var/www/html
Так как на просторах интернета я не нашел нормальный конфиг (оке, я просто не искал), а под рукой как раз docker, то решено было вытаскивать его из стандартного контейнера.
Уложилось все в 3 команды:
docker run -d httpd:2.4
docker ps
docker cp [ID контейнера]:/usr/local/apache2/conf/httpd.conf ./httpd.conf
После выполнения данных команд, в текущей директории появится файл httpd.conf, который мы и будем использовать в качестве основы.
По сути, это простое копирование из запущенного контейнера.
Создаем файл /httpd/httpd.conf в рабочей директории, который после правок выглядит так:
ServerRoot "/usr/local/apache2"
Listen 80
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule reqtimeout_module modules/mod_reqtimeout.so
LoadModule filter_module modules/mod_filter.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule env_module modules/mod_env.so
LoadModule headers_module modules/mod_headers.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule version_module modules/mod_version.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule status_module modules/mod_status.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule dir_module modules/mod_dir.so
LoadModule alias_module modules/mod_alias.so
# additional
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
ServerAdmin you@example.com
<Directory />
AllowOverride none
Require all denied
</Directory>
DocumentRoot "/var/www/html"
<Directory "/var/www/html">
Options Indexes FollowSymLinks Includes ExecCGI
AllowOverride All
Require all granted
</Directory>
<IfModule unixd_module>
User daemon
Group daemon
</IfModule>
<IfModule dir_module>
DirectoryIndex index.php index.pl index.cgi index.asp index.shtml index.html index.htm \
default.php default.pl default.cgi default.asp default.shtml default.html default.htm \
home.php home.pl home.cgi home.asp home.shtml home.html home.htm
</IfModule>
<Files ".ht*">
Require all denied
</Files>
<IfModule log_config_module>
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common
<IfModule logio_module>
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
</IfModule>
</IfModule>
<IfModule alias_module>
ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
</IfModule>
<Directory "/usr/local/apache2/cgi-bin">
AllowOverride All
Options None
Require all granted
</Directory>
<IfModule headers_module>
RequestHeader unset Proxy early
</IfModule>
<IfModule mime_module>
TypesConfig conf/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz
AddType text/html .shtml
AddHandler cgi-script .cgi .pl .asp
AddOutputFilter INCLUDES .shtml
</IfModule>
#
# Настройка FPM
#
<IfModule proxy_module>
<FilesMatch "\.php$">
SetHandler "proxy:fcgi://php:9000"
</FilesMatch>
</IfModule>
Создаем файл /nginx/nginx.conf в рабочей директории со следующим содержимым:
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location ~ \.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|js)$ {
root /var/www/html;
}
location ~ /\.ht {
deny all;
}
location / {
proxy_pass http://apache;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_connect_timeout 120;
proxy_send_timeout 120;
proxy_read_timeout 180;
}
}
}
В строке proxy_pass http://apache
мы опять указываем не IP адрес, а название контейнера (вспоминаем про магию с hosts).
Для тестинга, нам нужно будет проверить, работает ли PHP и работает ли Apache.
Сформируем такую структуру проекта:
- nginx
- nginx.conf
- httpd
- httpd.conf
- src
- protected
- .htaccess
- index.html
- index.php
- protected
- docker-compose.yml
Содержимое .htaccess:
Deny from all
Содержимое index.php:
<?php
phpinfo();
Содержимое index.html:
Apache not working :-(
Если все настроено корректно, то картина должна быть следующей:
- /index.php — откроется информация о php
- /protected/index.html — откроется 403 ошибка apache
- /protected/.htaccess — откроется 403 ошибка nginx (визуально они отличаются)
mariadb + phpmyadmin
https://hub.docker.com/_/mariadb/
https://hub.docker.com/r/phpmyadmin/phpmyadmin/
Поговорим про базы данных.
Конфиг для подключения выглядит следующим образом:
version: '3'
services:
mariadb:
image: mariadb:10.3
restart: always
volumes:
- ./mariadb:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: qwerty
phpmyadmin:
image: phpmyadmin/phpmyadmin
links:
- mariadb:db
ports:
- 8765:80
environment:
MYSQL_ROOT_PASSWORD: qwerty
depends_on:
- mariadb
Для mariadb и phpmyadmin мы указали директиву environment, которая содержит специфические для конкретного контейнера переменные (подробности можно посмотреть в репозиториях самих контейнеров).
В данном случае, это пароль для пользователя root.
Для phpmyadmin мы вручную указали директиву links:
links:
- mariadb:db
Сделать это необходимо для того, чтобы phpmyadmin знал с какой базой соединятся.
Если бы контейнер mariadb назывался db, то указывать эту директорию было бы не нужно.
Для mariadb мы слинковали директорию с данными:
volumes:
- ./mariadb:/var/lib/mysql
Сделано это для того, чтобы данные хранились в директории нашего проекта, а не внутри контейнера.
Если вы работаете не на linux машине, то у вас возникнут проблемы с размещением данных базы на локальной машине.
Эти непреодолимое обстоятельство, к сожалению, на данный момент не преодолено.
У кого есть решение просьба поделиться.
Однако по умолчанию (даже после уничтожения контейнера), данные базы сохраняются и вы можете пересоздавать контейнер сколько угодно раз — данные сохранятся в недрах локальной машины.
php + apache + nginx + mariadb + phpmyadmin
https://hub.docker.com/_/nginx/
https://hub.docker.com/_/php/
https://hub.docker.com/_/httpd/
https://hub.docker.com/_/mariadb/
https://hub.docker.com/r/phpmyadmin/phpmyadmin/
Ну, а теперь совмещаем наши конфиги, и получаем неплохое веб-окружение:
version: '3'
services:
apache:
image: httpd:2.4
volumes:
- ./src:/var/www/html
- ./httpd/httpd.conf:/usr/local/apache2/conf/httpd.conf
depends_on:
- php
nginx:
image: nginx:1.13
ports:
- 80:80
volumes:
- ./src:/var/www/html
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- apache
php:
build:
./php
volumes:
- ./src:/var/www/html
- ./php/php.ini:/usr/local/etc/php/php.ini
depends_on:
- mariadb
mariadb:
image: mariadb:10.3
volumes:
- ./mariadb:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: qwerty
phpmyadmin:
image: phpmyadmin/phpmyadmin
links:
- mariadb:db
ports:
- 8765:80
environment:
MYSQL_ROOT_PASSWORD: qwerty
depends_on:
- mariadb
Для php мы добавили директиву build (подробнее), в которой указали директорию php, где хранится Dockerfile со следующим содержанием:
FROM php:7.2-apache
RUN apt-get update
RUN docker-php-ext-install pdo pdo_mysql mysqli
В данном файле мы обновляем репозитории и устанавливаем php модули (подробнее про docker-php-ext-install).
Также мы слинковали конфиг php, чтобы можно было его настроить нужным нам образом.
Содержимое php.ini можно взять, например, здесь.
Запускаем, проверяем, радуемся!
Если все сделано правильно, то index.php отработает без ошибок, а в директории project/mysql появятся служебные файлы базы.
Docker production
По данному вопросу к сожалению я ничего сказать не могу, зато может сказать официальная документация.
Если у вас есть опыт использования docker на боевых проектах, то просьба поделиться своим опытом в комментариях: стоит ли, какие трудности и подводные камни у вас возникли и др. полезную информацию для молодых и неопытных.
Заключение
Вот собственно и все, чем я хотел поделиться.
Как видите необязательно знать принцип работы docker, чтобы успешно с ним работать.
Да, конечно, для тонкой настройки или сложных задач, необходимо уже углубляться в дебри docker, но в средне-статистическом случае — это не нужно.
Если у вас есть что добавить, или вы заметили какой-то косяк, или вы с чем-то не согласны, то прошу в комментарии, обсудим ;-)
Полезные ссылки (a.k.a список используемой литературы)
Документация официального сайта
Overview of Docker Compose (официальный сайт)
Полное практическое руководство по Docker: с нуля до кластера на AWS
Поняв Docker
Полная автоматизация «development» среды с помощью docker-compose
P.S.
Если честно, я не понимаю откуда столько негатива.
Судя по комментариям, основные претензии к формулировкам и терминологии, и это с учетом того, что в предисловии я написал что умышленно упрощаю многие моменты.
Цель статьи — показать, как просто работать с docker-compose, как вместо того, чтобы разворачивать 100500 окружений и ПО для каждого своего проекта, можно использовать docker-compose и быть довольным.
Тут нет речи про prodUction (одного абзаца хватит), про deploy, про миграцию между dev и prod средой.
Нет, статья не об этом.
P.P.S.
Большое спасибо krocos Caravus Vershnik Fesor за дельные комментарии.