Настройка веб-сервера в Docker (NGINX + PHP + MariaDB)

Thank you for reading this post, don't forget to subscribe! 
  • В каче­стве веб-сер­ве­ра будет исполь­зо­вать­ся NGINX.
  • Будет под­держ­ка при­ло­же­ний на PHP.
  • Мы созда­дим 2 кон­тей­не­ра Docker — один для NGINX + PHP, вто­рой для СУБД (MariaDB).
  • Веб-при­ло­же­ние будет заши­то в кон­тей­не­ре. Раз­дел для дан­ных MariaDB будет мон­ти­ро­вать­ся в каче­стве volume.

Дан­ную кон­фи­гу­ра­цию мож­но исполь­зо­вать для быст­ро­го раз­вер­ты­ва­ния сай­тов или для локаль­ной разработки.

NGINX + PHP + PHP-FPM

Реко­мен­ду­ет­ся каж­дый мик­ро­сер­вис поме­щать в свой отдель­ный кон­тей­нер, но мы (для отдель­но­го при­ме­ра) веб-сер­вер с интер­пре­та­то­ром PHP поме­стим в один и тот же имидж, на осно­ве кото­ро­го будут созда­вать­ся контейнеры.

Создание образа

Созда­дим ката­лог, в кото­ром будут нахо­дить­ся фай­лы для сбор­ки обра­за веб-сервера:

mkdir -p /opt/docker/web-server

Пере­хо­дим в создан­ный каталог:

cd /opt/docker/web-server/

Созда­ем докер-файл:

vi Dockerfile

[codesyntax lang="php"]

[/codesyntax]

* где:
1) ука­зы­ва­ем, какой берем базо­вый образ. В нашем слу­чае, CentOS 8.
3) зада­ем для инфор­ма­ции того, кто создал образ. Ука­зы­ва­ем свое имя и адрес элек­трон­ной почты.
5) созда­ем пере­мен­ную окру­же­ния TZ с ука­за­ни­ем вре­мен­ной зоны (в нашем при­ме­ре, мос­ков­ское время).
7) запус­ка­ем обнов­ле­ние системы.
8) уста­нав­ли­ва­ем паке­ты: веб-сер­вер nginx, интер­пре­та­тор php, сер­вис php-fpm для обра­бот­ки скрип­тов, модуль php-mysqli для рабо­ты php с СУБД MySQL/MariaDB.
9) уда­ля­ем ска­чан­ные паке­ты и вре­мен­ные фай­лы, обра­зо­вав­ши­е­ся во вре­мя установки.
10) добав­ля­ем в кон­фи­гу­ра­ци­он­ный файл nginx стро­ку daemon off, кото­рая запре­тит веб-сер­ве­ру авто­ма­ти­че­ски запу­стить­ся в каче­стве демона.
11) созда­ем ката­лог /run/php-fpm — без него не смо­жет запу­стить­ся php-fpm.
13) копи­ру­ем содер­жи­мое ката­ло­га html, кото­рый нахо­дит­ся в том же ката­ло­ге, что и dockerfile, в ката­лог /usr/share/nginx/html/ внут­ри кон­тей­не­ра. В дан­ной пап­ке дол­жен быть наше веб-приложение.
15) запус­ка­ем php-fpm и nginx. Коман­да CMD в dockerfile может быть толь­ко одна.
17) откры­ва­ем порт 80 для рабо­ты веб-сервера.

В рабо­чем ката­ло­ге созда­ем пап­ку html:

mkdir html

… а в ней — файл index.php:

vi html/index.php

[codesyntax lang="php"]

[/codesyntax]

* мы созда­ли скрипт, кото­рый будет выво­дить инфор­ма­цию о php в бра­у­зе­ре для при­ме­ра. По идее, в дан­ную пап­ку мы долж­ны поло­жить сайт (веб-при­ло­же­ние).

Созда­ем пер­вый билд для наше­го образа:

docker build -t test/webapp:v1 .

Новый образ дол­жен появить­ся в системе:

docker images

При жела­нии, его мож­но отпра­вить на Docker Hub сле­ду­ю­щи­ми командами:

docker login --username test

docker tag test/webapp:v1 test/web:nginx_php7

docker push test/web:nginx_php7

* пер­вой коман­дой мы про­шли аутен­ти­фи­ка­цию на пор­та­ле докер-хаба (в каче­стве id/login мы исполь­зу­ем test— это учет­ная запись, кото­рую мы заре­ги­стри­ро­ва­ли в Docker Hub). Вто­рая коман­да созда­ет тег для наше­го обра­за, где test— учет­ная запись на dockerhub; web — имя репо­зи­то­рия; nginx_php7 — сам тег. Послед­няя коман­да зали­ва­ет образ в репозиторий.
* подроб­нее про загруз­ку обра­за в репо­зи­то­рий докера.

Запуск контейнера и проверка работы

Запус­ка­ем веб-сер­вер из создан­но­го образа:

docker run --name web_server -d -p 80:80 test/webapp:v1

Откры­ва­ем бра­у­зер и пере­хо­дим по адре­су http://<IP-адрес сер­ве­ра с docker> — откро­ет­ся стра­ни­ца phpinfo:

Наш веб-сер­вер из Docker работает.

MariaDB

Для запус­ка СУБД мы будем исполь­зо­вать гото­вый образ mariadb. Так как после оста­нов­ки кон­тей­не­ра, все дан­ные внут­ри него уда­ля­ют­ся, мы долж­ны под­клю­чить внеш­ний том, на кото­ром будут хра­нить­ся наши базы.

Сна­ча­ла созда­ем том для докера:

docker volume create --name mariadb

* в дан­ном при­ме­ре мы созда­ли том с име­нем mariadb. Будет создан ката­лог /var/lib/docker/volumes/mariadb/_data/ на хосто­вом сер­ве­ре, куда будут раз­ме­щать­ся наши фай­лы базы.

Выпол­ним пер­вый запуск наше­го кон­тей­не­ра с mariadb:

docker run --rm --name maria_db -d -e MYSQL_ROOT_PASSWORD=password -v mariadb:/var/lib/mysql mariadb

* где:

  • --rm — уда­лить кон­тей­нер после оста­нов­ки. Это пер­вый запуск для ини­ци­а­ли­за­ции базы, после пара­мет­ры запус­ка кон­тей­не­ра будут другими.
  • --name maria_db — зада­ем имя кон­тей­не­ру, по кото­ро­му будем к нему обращаться.
  • -e MYSQL_ROOT_PASSWORD=password — созда­ем систем­ную пере­мен­ную, содер­жа­щую пароль для поль­зо­ва­те­ля root базы дан­ных. Остав­ля­ем его таким, как в дан­ной инструк­ции, так как сле­ду­ю­щим шагом мы его будем менять.
  • -v mariadb:/var/lib/mysql — гово­рим, что для кон­тей­не­ра мы хотим исполь­зо­вать том mariadb, кото­рый будет при­мон­ти­ро­ван внут­ри кон­тей­не­ра по пути /var/lib/mysql.
  • mariadb — в самом кон­це мы ука­зы­ва­ем имя обра­за, кото­рый нуж­но исполь­зо­вать для запус­ка кон­тей­не­ра. Это образ, кото­рый при пер­вом запус­ке будет ска­чан с DockerHub.

В ката­ло­ге тома долж­ны появить­ся фай­лы базы дан­ных. В этом мож­но убе­дить­ся командой:

ls /var/lib/docker/volumes/mariadb/_data/

Теперь под­клю­ча­ем­ся к команд­ной стро­ке внут­ри кон­тей­не­ра с сер­ве­ром базы данных:

docker exec -it maria_db /bin/bash

Под­клю­ча­ем­ся к mariadb:

:/# mysql -ppassword

Меня­ем пароль для учет­ной запи­си root:

> SET PASSWORD FOR 'root'@'localhost' = PASSWORD('New_Password');

Выхо­дим из команд­ной стро­ки СУБД:

> quit

Выхо­дим из контейнера:

:/# exit

Оста­нав­ли­ва­ем сам кон­тей­нер — он нам боль­ше не нужен с дан­ны­ми пара­мет­ра­ми запуска:

docker stop maria_db

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

docker run --name maria_db -d -v mariadb:/var/lib/mysql mariadb

Сер­вер баз дан­ных готов к работе.

Подключение к базе из веб-сервера

По отдель­но­сти, наши сер­ве­ры гото­вы к рабо­те. Теперь настро­им их таким обра­зом, что­бы из веб-сер­ве­ра мож­но было под­клю­чить­ся к СУБД.

Зай­дем в кон­тей­нер с базой данных:

docker exec -it maria_db /bin/bash

Под­клю­чим­ся к mariadb:

:/# mysql -p

Созда­дим базу дан­ных, если тако­вой еще нет:

CREATE DATABASE docker_db DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;

* в дан­ном при­ме­ре мы созда­ем базу docker_db.

Созда­ем поль­зо­ва­те­ля для досту­па к нашей базе данных:

> GRANT ALL PRIVILEGES ON docker_db.* TO 'docker_db_user'@'%' IDENTIFIED BY 'docker_db_password';

* и так, мы дали пол­ные пра­ва на базу docker_db поль­зо­ва­те­лю docker_db_user, кото­рый может под­клю­чать­ся от любо­го хоста (%). Пароль для дан­но­го поль­зо­ва­те­ля — docker_db_password.

Отклю­ча­ем­ся от СУБД:

> quit

Выхо­дим из контейнера:

:/# exit

Теперь пере­за­пу­стим наши кон­тей­не­ры с новым пара­мет­ром, кото­рый будет объ­еди­нять наши кон­тей­не­ры по внут­рен­ней сети.

Оста­нав­ли­ва­ем рабо­та­ю­щие кон­тей­не­ры и уда­ля­ем их:

docker stop maria_db web_server

docker rm maria_db web_server

Созда­ем docker-сеть:

docker network create net1

* мы созда­ли сеть net1.

Созда­ем новые кон­тей­не­ры из наших обра­зов и добав­ля­ем опцию --net, кото­рая ука­зы­ва­ет, какую сеть будет исполь­зо­вать контейнер:

docker run --name maria_db --net net1 -d -v mariadb:/var/lib/mysql mariadb

docker run --name web_server --net net1 -d -p 80:80 test/webapp:v1

* ука­зав опцию --net, наши кон­тей­не­ры начи­на­ют видеть друг дру­га по сво­им име­нам, кото­рые мы зада­ем опци­ей --name.

Гото­во. Для про­вер­ки соеди­не­ния с базой дан­ных в php мы можем исполь­зо­вать такой скрипт:

[codesyntax lang="php"]

[/codesyntax]

* в дан­ном при­ме­ре мы под­клю­ча­ем­ся к базе docker_db на сер­ве­ре maria_db с исполь­зо­ва­ни­ем учет­ной запи­си docker_db_user и паро­лем docker_db_password.

После его запус­ка, мы уви­дим либо пустой вывод (если под­клю­че­ние выпол­не­но успеш­но), либо ошибку.

Использование docker-compose

В отли­чие от docker, с помо­щью docker-compose мож­но раз­во­ра­чи­вать про­ек­ты, состо­я­щие из несколь­ких кон­тей­не­ров, одной командой.

И так, авто­ма­ти­зи­ру­ем запуск наших кон­тей­не­ров с исполь­зо­ва­ни­ем docker-compose. Необ­хо­ди­мо, что­бы он был уста­нов­лен в системе.

Сна­ча­ла уда­лим кон­тей­не­ры, кото­рые созда­ли на преды­ду­щих этапах:

docker rm -f web_server maria_db

Пере­хо­дим в ката­лог для наших сборок:

cd /opt/docker/

Созда­ем yml-файл с инструк­ци­я­ми сбор­ки кон­тей­не­ров через docker-compose:

vi docker-compose.yml

[codesyntax lang="php"]

[/codesyntax]

* в фор­ма­те yml очень важ­ное зна­че­ние име­ют отсту­пы. Если сде­лать лиш­ний про­бел, то мы полу­чим ошибку.
* где:

  • version — вер­сия фай­ла yml. На стра­ни­це docs.docker.com пред­став­ле­на таб­ли­ца, поз­во­ля­ю­щая понять, какую вер­сию луч­ше исполь­зо­вать, в зави­си­мо­сти от вер­сии docker (docker -v).
  • services — docker-compose опе­ри­ру­ет сер­ви­са­ми, где для каж­до­го созда­ет­ся свой блок опи­са­ния. Все эти бло­ки вхо­дят в раз­дел services.
  • build — опции сбор­ки. В нашем при­ме­ре для веб-сер­ве­ра мы долж­ны собрать имидж.
  • context — ука­зы­ва­ем путь до Dockerfile.
  • args — поз­во­ля­ет задать аргу­мен­ты, кото­рые доступ­ны толь­ко в про­цес­се сбор­ки. В дан­ном при­ме­ре мы исполь­зу­ем толь­ко аргу­мент с ука­за­ни­ем номе­ра сборки.
  • container_name — зада­ем имя, кото­рое будет зада­но кон­тей­не­ру после его запуска.
  • restart — режим пере­за­пус­ка. В нашем слу­чае все­гда, таким обра­зом, после пере­за­груз­ки сер­ве­ра, наши кон­тей­не­ры запустятся.
  • ports — при необ­хо­ди­мо­сти, ука­зы­ва­ем порт, кото­рый будет наш сер­вер про­бра­сы­вать запрос внутрь контейнера.
  • volumes — поз­во­ля­ет внутрь кон­тей­не­ра про­ки­нуть ката­лог сер­ве­ра. Таким обра­зом, важ­ные дан­ные не будут являть­ся частью кон­тей­не­ра и не будут уда­ле­ны после его остановки.

Запус­ка­ем сбор­ку наших кон­тей­не­ров с помо­щью docker-compose:

docker-compose build

Запус­ка­ем кон­тей­не­ры в режи­ме демона:

docker-compose up -d

Про­ве­ря­ем, какие кон­тей­не­ры запущены:

docker ps

Возможные проблемы

Рас­смот­рим неко­то­рые про­бле­мы, кото­рые могут воз­ник­нуть в про­цес­се настройки.

1. Errors during downloading metadata for repository 'AppStream'

Ошиб­ка воз­ни­ка­ет при попыт­ке собрать имидж на Linux CentOS 8. Пол­ный текст ошиб­ки может быть такой:

Errors during downloading metadata for repository 'AppStream':
- Curl error (6): Couldn't resolve host name for http://mirrorlist.centos.org/?release=8&arch=x86_64&repo=AppStream&infra=container [Could not resolve host: mirrorlist.centos.org]
Error: Failed to download metadata for repo 'AppStream': Cannot prepare internal mirrorlist: Curl error (6): Couldn't resolve host name for http://mirrorlist.centos.org/?release=8&arch=x86_64&repo=AppStream&infra=container [Could not resolve host: mirrorlist.centos.org]

При­чи­на: систе­ма внут­ри кон­тей­не­ра не может раз­ре­шить dns-име­на в IP-адрес.

Реше­ние: в CentOS 8 запро­сы DNS могут бло­ки­ро­вать­ся бранд­мау­э­ром, когда в каче­стве сер­вер­ной части (backend) сто­ит nftables. Пере­клю­че­ние на iptables реша­ет про­бле­му. Откры­ва­ем файл:

vi /etc/firewalld/firewalld.conf

Нахо­дим строку:

FirewallBackend=nftables

… и меня­ем ее на:

FirewallBackend=iptables

Пере­за­пус­ка­ем сер­вис firewalld:

systemctl restart firewalld