Добрый день, уважаемый Хабр!
Скажу сразу, что я — разработчик, и, думаю многие коллеги меня поддержат, в наших проектах часто не хватает системных администраторов, где-то сначала, где-то всегда. Нам приходится изучать новые для себя материалы в данной области, а делиться друг с другом накопленным опытом, по-моему, правильно. Данная статья не претендует на лавры завершенного руководства по настройке 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-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/$hostname.error.log;
    access_log /var/log/nginx/$hostname.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/$hostname.error.log;
       access_log /var/log/nginx/$hostname.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
chown -R ${USER_NAME}:${USER_NAME} /var/lib/php/session/

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

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

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


  1. denaspireone
    03.08.2018 10:28

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


    1. iAmWeb Автор
      03.08.2018 10:46

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


      1. denaspireone
        03.08.2018 10:52
        +1

        На официальном сайте symfony всегда конфиг для apache2/nginx всегда имеется в наличии, даже на сайте nginx есть конфиги для symfony ранних версий, до 4ки тожно.

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


  1. jehy
    03.08.2018 10:37

    Для тех, кого, как и меня, смутило 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.


    1. iAmWeb Автор
      03.08.2018 10:49

      Да, LEMP — это связка Linux, Nginx, MySQL и PHP. Вариация знакомого всем LAMP, где вместо Apache используется Nginx.


  1. VolCh
    03.08.2018 11:36

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

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


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


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


  1. iAmWeb Автор
    03.08.2018 11:44

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

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


    1. jenki
      03.08.2018 15:17

      Я хотел показать именно минимальную конфигурацию.
      Таких статей пруд пруди, на разных языках. В чём исключительное отличие от остальных? Установка дополнительных репозиториев или отключение мандатной системы контроля доступа?
      Статья просто в радость для майнеров.


      1. iAmWeb Автор
        03.08.2018 15:21

        А что майнеры не люди? Будет полезна им и хорошо. Я еще раз повторюсь, что собирал эту информацию по кускам и немалое время, всей информации в одном месте я не встречал, потому и решил ей поделиться.


        1. denaspireone
          03.08.2018 15:51

          Обявляем неделю старых новых полезных мануалов


        1. VJean
          03.08.2018 17:16
          +1

          Статья просто в радость для майнеров.

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

          Спасибо, вы сделали мой день.


  1. rt3879439
    03.08.2018 12:12

    1. jenki
      03.08.2018 17:54

      Увы, его мало кто читает, ещё меньше пытаются вникнуть о чем речь.


  1. tnt4brain
    03.08.2018 19:49
    +2

    Увы, после команды отключения SELinux начал прокручивать остаток текста до комментариев. Интересуюсь для собственного понимания, стоит ли писать статью — а что именно помешало воспользоваться штатными инструментами ОС (audit2why, audit2allow) и сразу создать недостающие политики?


    1. jenki
      06.08.2018 00:06

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


  1. Amet13
    03.08.2018 20:16
    +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/ возможно там уже есть задание на обновление, по крайней мере в убунту они вместе с установкой пакета добавляется.


    1. VJean
      03.08.2018 20:49

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

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


      1. Amet13
        03.08.2018 20:54

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

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

        Это да, но для меня лучше смотрится в мануале например, когда нет лишнего, а только то, что нужно сделать.


        1. OnYourLips
          04.08.2018 23:27
          +1

          Что-то менять руками на рабочем сервере — не самая лучшая идея. Для этого есть инструменты автоматизации.


          1. VolCh
            04.08.2018 23:37

            Если есть кому автоматизировать.


            1. OnYourLips
              05.08.2018 10:40
              +1

              Так если некому (нет ни админов, ни программистов), то не будет и задачи устанавливать что-то на сервер.


              1. VolCh
                05.08.2018 11:12

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


    1. iAmWeb Автор
      04.08.2018 13:24

      Огромное спасибо за советы, замечания внедрил. По поводу перезагрузки согласен с VJean, и именно потому, что это чистая установка, оставил ее в самом конце.


  1. mikkisse
    03.08.2018 20:54
    +3

    И так всегда

    Отключим SELINUX: