LEMP стек c PHP 7 на CentOS 7 + Let's Encrypt в Google Cloud для развертывания приложения Symfony 4

Добрый день, уважаемый Хабр!
Скажу сразу, что я — разработчик, и, думаю многие коллеги меня поддержат, в наших проектах часто не хватает системных администраторов, где-то сначала, где-то всегда. Нам приходится изучать новые для себя материалы в данной области, а делиться друг с другом накопленным опытом, по-моему, правильно. Данная статья не претендует на лавры завершенного руководства по настройке CentOS, и да, в ней отключается SELinux, и не создаются дополнительные пользователи MySQL, она описывает лишь то, что сказано в заголовке при чем в минимальной рабочей конфигурации. В конфигурации, которую затем всегда может дополнить специалист, но которая позволит развернуть требуемый проект за максимально короткое время. Я буду благодарен гуру за их советы и замечания, всегда за конструктивную критику, на основании которой буду вносить правки, если потребуется. Для остальных же хочу процитировать AntonShevchuk: «Если Вы не согласны с автором статьи — опишите свою точку зрения, зачем же злорадно понижать ему карму?». Спасибо, поехали…

В данном посте я приведу конкретные шаги по установке и настройке связки Nginx + MySQL + PHP7 на CentOS 7. Стоит отметить, что в данной статье будет рассказано про настройку системы для одного домена. В качестве площадки будет использоваться инстанс на Google Cloud Platform, с создания которого и начну:

Создание экземпляра в Google Cloud с генерацией SSH-ключей
Выберем в главном меню выбираем «Compute Engine» и нажимаем «Создать экземпляр». В зависимости от предполагаемой нагрузки выбираем тип машины, но не ниже g1-small, так как Composer для Symfony 4 требователен к оперативной памяти и на f1-micro загрузить зависимости не сможет.

Предлагаемый образ загрузочного диска изменяем на CentOS 7, минимальный объем диска в 10Гб вполне подойдет для развертывания системы, всех необходимых библиотек и зависимостей.

Для доступа к создаваемому серверу по SSH необходимо сгенерировать соответствующий ключ, у пользователей MacOS X или Linux для этого существует команда

ssh-keygen -t rsa -f __FILE__ -C __USER__

где __FILE__ — путь к сохраняемому ключу с именем файла, а __USER__ — имя пользователя под которым вы будете логинититься в систему. При выполнения данной команды система запросит вас указать ключевую фразу, которая будет служить паролем при использовании данного ключа, ее ввод необязателен, для пропуска ничего не вводите и нажмите Enter, далее подтвердите предыдущее действие. После выполнения данной команды система создаст указанный в __FILE__ приватный ключ, а рядом с ним — открытый с именем __FILE__.pub
Последнее, что остается сделать с ключом пользователям MacOS X и Linux — это запретить доступ на запись для всех, кроме владельца ключа, делается это командой

chmod 400 __FILE__ 

Пользователи Windows могут вопользоваться программой PuTTYgen. Откройте программу, нажмите Generate и следуйте инструкциям, установленные по-умолчанию параметры подходит для большинства случаев, однако Google настаивает на 2048-битных ключах, не забудьте установить данный параметр. Поле «Key comment» служит для задания имени пользователя, а для задания ключевой фразы — «Key passphrase». После завершения генерации программа отобразит публичный ключ. Чтобы сохранить приватный ключ с расширением .ppk нажмите «Save private key».

Скопируйте содержимое открытого ключа (из файла __FILE__.pub для MacOS X / Linux или из окна PuTTYgen для Windows) и вставьте его в соответствующее поле на вкладке «Безопасность»:



Не забываем разрешить трафик HTTP и HTTPS, поставив галочки в соответствующих полях. На данном этапе все готово, нажимаем «Создать».

После создания система отобразит присвоенный экземпляру внешний IP-адрес, подключимся к нему по SSH используя приватный ключ:

ssh -i __FILE__ __USER__@__IP__


Приступим непосредственно к установке LEMP. Под правами текущего пользователя создайте переменные для будущего использования:

export USER_NAME=__ИМЯ_ПОЛЬЗОВАТЕЛЯ_СИСТЕМЫ__
export DOMAIN_NAME=__ВАШЕ_ДОМЕННОЕ_ИМЯ__

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

Обновляем систему и удаляем «осиротевшие» пакеты:

yum clean all
yum update
yum autoremove

Добавляем поддержку репозиториев Fedora, установим текстовый редактор nano и утилиту wget для возможности получения файлов с удаленных серверов. Для развертывания приложений на Symfony (и не только), я использую клонирование git-репозитория, для реализации данного метода установим поддержку git, также Symfony необходима поддержка zip (опция -y означает автоматический ответ «Y» на все возникающие у инсталлятора вопросы):

yum install epel-release nano wget git-core zip unzip -y

Нужно указать корректное имя хоста, для этого отредактируем конфигурационный файл сети:

cat << EOF > /etc/sysconfig/network
HOSTNAME=${DOMAIN_NAME}
EOF


Сохраняем файл и устанавливаем его же с помощью утилиты hostnamectl:

hostnamectl set-hostname ${DOMAIN_NAME}

Устанавливаем nginx:

yum install nginx -y

Запускаем nginx:

systemctl start nginx

Добавляем nginx в автозагрузку:

systemctl enable nginx

Устанавливаем MySQL, запускаем и добавляем в автозагрузку:

yum install mariadb mariadb-server -y
systemctl start mariadb
systemctl enable mariadb

Проведем начальную настройку MySQL, на все вопросы скрипта ответьте «Y» и укажите пароль суперпользователя:

mysql_secure_installation

Добавим поддержку Let's Encrypt:

yum install certbot -y

Так как к моменту написания данной статьи PHP 7 нет ни в репозиториях CentOS, ни в Fedora, его мы установим из репозитория REMI:

yum install http://rpms.remirepo.net/enterprise/remi-release-7.rpm yum-utils -y

yum-config-manager --enable remi-php72

yum --enablerepo=remi,remi-php72 install php-fpm php-common php-opcache php-cli php-pear php-pdo php-mysqlnd php-gd php-mcrypt php-xml php-zip -y

Весь необходимый софт установлен, перейдем к его настройке, первоначально отредактируем конфигурационный файл php-fpm:

nano /etc/php-fpm.d/www.conf

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

user = __ИМЯ_ПОЛЬЗОВАТЕЛЯ_СИСТЕМЫ__
group = __ИМЯ_ПОЛЬЗОВАТЕЛЯ_СИСТЕМЫ__
listen.owner = __ИМЯ_ПОЛЬЗОВАТЕЛЯ_СИСТЕМЫ__
listen.group = __ИМЯ_ПОЛЬЗОВАТЕЛЯ_СИСТЕМЫ__

Далее, укажем сокет — необходимо под строкой listen = 127.0.0.1:9000 добавить следующее:

listen = /var/run/php-fpm/php-fpm.sock

И последнее, в конце файла измените папку для хранения файлов сессий:

php_value[session.save_path]    = /home/__ИМЯ_ПОЛЬЗОВАТЕЛЯ_СИСТЕМЫ__/__ВАШЕ_ДОМЕННОЕ_ИМЯ__/var/sessions

Сохраните изменения, затем запустите и добавьте php-fpm в автозагрузку:

systemctl start php-fpm
systemctl enable php-fpm

Отключим SELINUX:

setenforce 0

Данной командой вы отключаете SELINUX только в текущем сеансе пользователя, чтобы после перезагруки настройки сохранились необходимо отредактировать его конфигурационный файл:

nano /etc/selinux/config

где указать SELINUX=disabled

Сохраните изменения и выйдете из режима суперпользователя. Далее мы создадим директорию для проекта в домашней директории и поместим туда индексный файл — заглушку для проверки работоспособности создаваемого стека:

cd
mkdir -p ${DOMAIN_NAME}/public
cd ${DOMAIN_NAME}/public
nano index.php

Пусть содержимое файла будет следующим (замените __YOUR_ROOT_PASSWORD__ на пароль суперпользователя MySQL, указанный при его настройке):

<html>
<head>
    <h2>LEMP test</h2>
</head>
    <body>
    <?php echo '<p>Hello!</p>';

    $host = "localhost";
    $username = "root";
    $password = "__YOUR_ROOT_PASSWORD__";

    $connection = mysqli_connect($host, $username, $password);

    if (!$connection) 
        print '<p>DB connect failed with error: ' .  mysqli_connect_error() . '</p>';
    else
        print '<p>DB connection established</p>';
    ?>
</body>
</html>

Снова авторизуемся под правами суперпользователя и откроем на редактирование конфигурационный файл nginx:

nano /etc/nginx/nginx.conf

Изменим имя пользователя подобно конфигурации php-fpm:
user __ИМЯ_ПОЛЬЗОВАТЕЛЯ_СИСТЕМЫ__;

Конфигурацию директивы server, созданную по-умолчанию, измените следующим образом:

server {
    set $USER_NAME __ИМЯ_ПОЛЬЗОВАТЕЛЯ_СИСТЕМЫ__;

    server_name $hostname www.$hostname;

    root /home/$USER_NAME/$hostname/public;

    location / {
        # try to serve file directly, fallback to index.php
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;

        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;

        internal;
    }

    location ~ \.php$ {
        return 404;
    }

    error_log /var/log/nginx/__ВАШЕ_ДОМЕННОЕ_ИМЯ__.error.log;
    access_log /var/log/nginx/__ВАШЕ_ДОМЕННОЕ_ИМЯ__.access.log;
}

Сохраните изменения и дайте команду nginx перечитать конфигурацию:

nginx -s reload

По доменному имени __ВАШЕ_ДОМЕННОЕ_ИМЯ__ должна открываться созданная нами страница — заглушка.

Приступим к созданию сертификата:

openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

certbot certonly --webroot -w /home/${USER_NAME}/${DOMAIN_NAME}/public -d ${DOMAIN_NAME}

certbot certonly --webroot -w /home/${USER_NAME}/${DOMAIN_NAME}/public -d www.${DOMAIN_NAME}

После генерации ключей, изменим конфигурацию nginx для поддержки HTTPS, в данной конфигурации «главным» идет домен без www и настроено перенаправление на защищенное соединение. И так:

nano /etc/nginx/nginx.conf

Меняем созданную нами 2 шага назад директиву server на следующие:

server {
       set $USER_NAME __ИМЯ_ПОЛЬЗОВАТЕЛЯ_СИСТЕМЫ__;
       listen 443 ssl;
       server_name $hostname;

       ssl_certificate /etc/letsencrypt/live/__ВАШЕ_ДОМЕННОЕ_ИМЯ__/fullchain.pem;
       ssl_certificate_key /etc/letsencrypt/live/__ВАШЕ_ДОМЕННОЕ_ИМЯ__/privkey.pem;

       ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
       ssl_prefer_server_ciphers on;
       ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
       ssl_ecdh_curve secp384r1;
       ssl_session_cache shared:SSL:10m;
       ssl_session_tickets off;
       ssl_stapling on;
       ssl_stapling_verify on;
       resolver 8.8.8.8 8.8.4.4 valid=300s;
       resolver_timeout 5s;
       add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
       add_header X-Frame-Options DENY;
       add_header X-Content-Type-Options nosniff;

       ssl_dhparam /etc/ssl/certs/dhparam.pem;

       root /home/$USER_NAME/$hostname/public;

       location / {
           # try to serve file directly, fallback to index.php
           try_files $uri /index.php$is_args$args;
       }

       location ~ ^/index\.php(/|$) {
           fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
           fastcgi_split_path_info ^(.+\.php)(/.*)$;
           include fastcgi_params;

           fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
           fastcgi_param DOCUMENT_ROOT $realpath_root;

           internal;
       }

       location ~ \.php$ {
           return 404;
       }

       error_log /var/log/nginx/__ВАШЕ_ДОМЕННОЕ_ИМЯ__.error.log;
       access_log /var/log/nginx/__ВАШЕ_ДОМЕННОЕ_ИМЯ__.access.log;
   }

   server {
       listen 443 ssl;
       server_name www.$hostname;

       ssl_certificate /etc/letsencrypt/live/www.__ВАШЕ_ДОМЕННОЕ_ИМЯ__/fullchain.pem;
       ssl_certificate_key /etc/letsencrypt/live/www.__ВАШЕ_ДОМЕННОЕ_ИМЯ__/privkey.pem;
       ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

       return 301 https://$hostname$request_uri;
   }

   server {
       server_name $hostname www.$hostname;
       return 301 https://$hostname$request_uri;
   }

Сохраняем результат и говорим nginx перечитать конфигурацию:

nginx -s reload

Так как Let's Encrypt необходимо обновлять раз в три месяца, настроим автоматическое продление в планировщике, к примеру так, в ночь на понедельник

crontab -e

15 4 * * 1 /usr/bin/certbot renew --quiet
18 4 * * 1 /usr/bin/systemctl reload nginx

Чтобы у разворачиваемого приложения не было проблем с правами, меняем владельца следующей папки:

chown -R ${USER_NAME}:${USER_NAME} /var/lib/nginx

Все, система готова. Перезагрузитесь, чтобы удостовериться, что весь необходимый софт сконфигурирован правильно и после перезагрузки не будет вести себя иначе, далее можно удалить созданную нами папку проекта в директории пользователя и клонировать в ней необходимый рабочий репозиторий. Не забудьте создать папку для хранения сессий пользователей:

mkdir -p ${DOMAIN_NAME}/var/sessions

UPD:
По совету Amet13 в командах используются переменные + объединение пакетов в последовательную «бесшумную» установку

UDP 2:
В настройках PHP-FPM изменена папка для хранения сессий на «локальную» папку проекта, чтобы предотвратить проблемы с правами доступа
Поделиться публикацией

Комментарии 24

    +1
    а что не так с lemp стеком? все вроде стандартно и типично
    ничего нового и прям таки отличного от стандартной натсройки и установки нет, разве что можно было заиспользовать MySQL от самого GCE — чего здесь нет кстати…

      –1
      В данном случае настройка для хоста в nginx подойдет именно для приложений Symfony, для которого и писалась. Эта статья задумывалась как туториал для тех, кому нужны конкретные шаги для сборки системы в минимальной конфигурации под Symfony проект, с самого начала. Я собирал по кускам, обновлял и дописывал информацию из различных источников, которую и решил оформить в виде данного руководства, мало ли кому то поможет.
        +1
        На официальном сайте symfony всегда конфиг для apache2/nginx всегда имеется в наличии, даже на сайте nginx есть конфиги для symfony ранних версий, до 4ки тожно.

        PS: при этом Вы не указали пример для production server и test installation. Так же не првиели ссылок на оф сайты с доками. Я вот редко когда доверяю отсебятине, т.к. всегда делаю сам и перепроверяю.
        PSS: положите все готовые конфиги на github — это сделает более полезной этот пост к.м.к.
      +1
      Для тех, кого, как и меня, смутило LEMP вместо LAMP или LNMP:

      We go with LEMP due to the pronunciation for Nginx: Engine-X (en-juhn-ecks). Think of how in English, the article an is used instead of a for hour even though it begins with a consonant. The importance is the sound of the first letter rather than its written representation. Besides, LEMP is actually pronounceable and doesn’t sound like reciting the alphabet.
        0
        Да, LEMP — это связка Linux, Nginx, MySQL и PHP. Вариация знакомого всем LAMP, где вместо Apache используется Nginx.
        0
        заглушку для проверки работоспособности создаваемого стека:
        Пусть содержимое файла будет следующим (замените YOUR_ROOT_PASSWORD на пароль суперпользователя MySQL, указанный при его настройке):

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


        Нет мелочей в безопасности. Даже для тестирования нужно пароли помещать, как минимум, в отдельный файл, вне докрута.


        Кстати, для тестирования симфони+доктрина я бы проверял не через mysqli, а через pdo.

          0
          Нет мелочей в безопасности

          Все так. Вполне можно создать отдельного пользователя и все манипуляции проводить с ним, никто не мешает, я могу дополнить статью, если пожелаете. Я хотел показать именно минимальную конфигурацию. Пароль суперпользователя MySQL изначально пуст, это знают все и многие этим пренебрегают. Мы же при mysql_secure_installation устанавливаем пароль и запрещаем внешний коннект под рутом.
            0
            Я хотел показать именно минимальную конфигурацию.
            Таких статей пруд пруди, на разных языках. В чём исключительное отличие от остальных? Установка дополнительных репозиториев или отключение мандатной системы контроля доступа?
            Статья просто в радость для майнеров.
              0
              А что майнеры не люди? Будет полезна им и хорошо. Я еще раз повторюсь, что собирал эту информацию по кускам и немалое время, всей информации в одном месте я не встречал, потому и решил ей поделиться.
                0
                Обявляем неделю старых новых полезных мануалов
                  +1
                  Статья просто в радость для майнеров.

                  А что майнеры не люди? Будет полезна им и хорошо.

                  Спасибо, вы сделали мой день.
              +2
                0
                Увы, его мало кто читает, ещё меньше пытаются вникнуть о чем речь.
                +2
                Увы, после команды отключения SELinux начал прокручивать остаток текста до комментариев. Интересуюсь для собственного понимания, стоит ли писать статью — а что именно помешало воспользоваться штатными инструментами ОС (audit2why, audit2allow) и сразу создать недостающие политики?
                  0
                  Утилиты audit2why, audit2allow не сильно подходят для написания политики (именно политики), но и с разрешающими правилами надо быть внимательным, audit2allow часто слишком фривольные правила выдаёт. Другой момент — для данного стэка все политики уже давно идут по умолчанию и ничего не надо писать дополнительно.
                  +1

                  Немного замечаний по Linux.


                  Не нужно каждый раз после изменения nginx перезагружать его, достаточно указать, чтобы перечитал конфиг:


                  nginx -s reload

                  Даунтайма сервиса в таком случае не будет, конкретно в этом случае это не критично, но лучше сразу привыкать делать правильно.


                  Вместо того чтобы писать:


                  yum install package1
                  yum install package2

                  как по мне лучше использовать команду


                  yum install package1 package2 -y

                  для удобства чтения.


                  Перезагружать сервер после всех настроек также не самая лучшая практика, достаточно проверить сервисы systemd, что они в состоянии enabled.


                  Если есть какие-то переменные, то лучше вместо этого:
                  __USER__ и <domain.name>
                  использовать:


                  export USER_NAME=vasya
                  export DOMAIN_NAME=example.com
                  chown -R ${USER_NAME}:${USER_NAME} /var/www/${DOMAIN_NAME}

                  Вместо cd ~ достаточно просто cd


                  Вместо


                  # wget http://rpms.remirepo.net/enterprise/remi-release-7.rpm
                  # rpm -Uvh remi-release-7.rpm

                  лучше юзать:


                  yum install http://rpms.remirepo.net/enterprise/remi-release-7.rpm

                  Вместо:


                  # hostname <domain.name>
                  # /etc/init.d/network restart

                  лучше использовать:


                  hostnamectl set-hostname $YOUR_HOSTNAME

                  Вместо того чтобы писать:


                  nano file.txt
                  и добавляем сюда строчки
                  foo=bar

                  лучше использовать конструкцию:


                  cat << EOF > file.txt
                  foo=bar
                  ...
                  EOF

                  По поводу обновления сертификатов, проверьте в /etc/cron.d/ возможно там уже есть задание на обновление, по крайней мере в убунту они вместе с установкой пакета добавляется.

                    0
                    Перезагружать сервер после всех настроек также не самая лучшая практика, достаточно проверить сервисы systemd, что они в состоянии enabled.
                    Спорно, перезагрузкой как раз проверяется что всё будет работать в случае ребута через веб-морду или запуска после выключения. В дальнейшем, когда давным давно подняты все нужные сервисы, уже смотреть на состояние systemd после правок.

                    cat << EOF > file.txt
                    Таки быстрее открыть редактор с подсветкой синтаксиса и написать что нужно, чем вспоминать очередность кавычек в консоле и прописывать EOF.
                      0
                      Про перезагрузку, на данном этапе, когда это чистая установка, вполне допустимо, но лучше к этому не привыкать, в дальнейшей перспективе когда уже будет рабочая инсталляция, перезагружать сервер явно не будете после изменения настроек. Но это дело привычки скорее.

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

                      Это да, но для меня лучше смотрится в мануале например, когда нет лишнего, а только то, что нужно сделать.
                        +1
                        Что-то менять руками на рабочем сервере — не самая лучшая идея. Для этого есть инструменты автоматизации.
                          0
                          Если есть кому автоматизировать.
                            +1
                            Так если некому (нет ни админов, ни программистов), то не будет и задачи устанавливать что-то на сервер.
                              0
                              Есть и программисты, и админы, но в их обязанности не входит автоматизация процессов разработки и деплоя. Админы ручками на сервера ходят и необходимости в автоматизации не видят, да и опасаются, что они ненужными станут. А программисты бизнес-задачи решают.
                      0
                      Огромное спасибо за советы, замечания внедрил. По поводу перезагрузки согласен с VJean, и именно потому, что это чистая установка, оставил ее в самом конце.
                      +3
                      И так всегда
                      Отключим SELINUX:

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое