Собрался наконец-то перенести RTFM на Debian 10, решил делать без автоматизации — будем поднимать стандартный LEMP для хостинга WordPress руками.
Что-то похожее последний раз писалось в 2016 — Debian: установка LEMP — NGINX + PHP-FPM + MariaDB, в этот раз получился более полный обзор процесса.
Также, когда-то делал автоматизацию для настройки сервера под RTFM, но последний раз она использовалась в 2018 (см. AWS: миграция RTFM 3.0 (final) — CloudFormation и Ansible роли), а теперь многое поменялось/добавилось, и переделывать заново смысла нет — та автоматизация делалась больше для того, что бы потрогать Ansible/CloudFormation и вообще автоматизацию, чем для реального применения.
И снова — планировался краткий пост чисто по установке NGINX + PHP + MySQL, а в результате получается длинност по сетапу полноценного сервера — и мониторинг, и логи, и вот это вот всё.
Что будем делать:
- создадим дроплет в DigitalOcean
- SSL — используем Let’s Encrypt
- NGINX
- PHP-FPM
- MySQL (MariaDB) в роли базы данных
- NGINX Amplify agent — мониторинг и алертинг (Grafana и Loki не прижились, т.к. жрут слишком много ресурсов, но сам сетап был интересный, см. Prometheus: мониторинг для RTFM — Grafana, Loki и promtail)
- WordPress backup script — самописный скрипт на Python, см. Python: скрипт бекапа файлов и баз MySQL в AWS S3
- Logz.io — сбор логов в ELK-стек
unattended-upgrades
— автоапдейты Debian и пакетовlogrotate
— ротация логов
Содержание
DigitalOceal: создание дроплета
Переехал на DigitalOcean из AWS кажется в прошлом году.
Причина — стоимость, так как DigitalOcean обходится намного дешевле. Работает в целом без нареканий, потому пока тут и остаюсь.
Создаём дроплет:
Будет Debian 10, 2 CPU, 2 GB RAM.
Например, на текущем сервере той же конфигурации нагрузка и потребление памяти выглядят так (график из NGINX Amplify):
Выбираем систему и тип инстанса:
Выберем Франкфурт, и включим дополнительный мониторинг — на дроплете сразу будет запущен DigitalOcean-агент, который отрисует больше графиков в панели управления самого DO:
Создание RSA ключа для SSH
На рабочей машине создаём новую пару ключа:
Копируем содержимое публичной части:
В DO создаём новый ключ:
Выбираем количество дроплетов 1, и задаём имя хоста rtfm-do-production-d10:
По желанию — включаем бекапы, и создаём дроплет:
Firewall
Пока создаётся дроплет — добавим для него фаервол:
Добавляем правила — SSH, ICMP — ограниченные только IP, на котором я сейчас, HTTP/S — отовсюду, хотя на время миграции имеет смысл ограничить доступ, что бы Google не проиндексировал блог, как копию оригинального сайта:
Подключаем к новому дроплету:
Floating IP
Аналог Elasitc IP в AWS, добавляем новый IP для нового сервера:
Тут, собственно, всё.
Теперь к серверу.
LEMP — Linux, NGINX, PHP, MySQL
Ещё раз — что нам надо будет насетапить на сервере?
- nginx
- php-fpm
- lets encrypt
- mysql
- amplify agent — мониторинг
- backup script
- logz.io — есть free-доступ, правда логи хранит только деень — но мне больше и не надо
- unattended-upgrades — автоапдейты системы и установленных пакетов
- logrotate — на Debian есть по дефолту, проверим его настройки
- msmtp — для отправки почты
Подключаемся на хост:
Обновляем систему, перезагружаемся:
Устанавливаем пакеты для LEMP:
Проверяем NGINX:
Устанавливаем прочие полезные пакеты:
С mailutils
есть проблема при использовании mailx
с msmtp
, пришлось заменить на bsd-mailx
, см. mailx и msmtp — отправка почты с сервера.
Let’s Encrypt SSL
Используем сертификат от Let’s Encrypt.
Let’s Encrypt DNS validation
Возникает вопрос по валидации домена при получении сертификата — ведь сейчас домен направлен на старый сервер, значит стандартный метод с валидацией через веб-сервер и .well-known
директорию не подойдёт.
Что сделаем — используем DNS-валидацию при получении сертификата, а потом, когда уже будет сертификат и настроенный NGINX — перенастроим certbot
на валидацию через веб-сервер, т.к. DNS-валидация не поддерживает (но это не точно) renew
сертификатов.
Стопаем NGINX:
Получаем сертификат:
На DNS домена добавляем запись:
Проверяем:
Возвращаемся к серверу, жмём Enter — готово:
NGINX
Генерируем DF-ключ (см. ClientKeyExchange
) для NGINX:
Удаляем конфиг default
— у нас RTFM будет дефолтным виртуалхостом:
Создаём конфиг виртуалхоста для РТФМ — /etc/nginx/conf.d/rtfm.co.ua.conf
. Он есть на старом сервере, можно просто скопировать.
У меня тут достаточно длинный конфиг получился, переношу уже его уже лет с 5 разных серверов, не меняется ничего кроме настроек SSL.
Последний тест на
Вообще, удобная штука генераторы конфигов, например
В моём конфиге под WordPress дополнительно блокируется доступ к /wp-admin
и wp-login.php
:
server { listen 80 default_server; server_name rtfm.co.ua www.rtfm.co.ua; server_tokens off; return 301 https://rtfm.co.ua$request_uri; } server { listen 443 ssl default_server; server_name rtfm.co.ua; root /data/www/rtfm/rtfm.co.ua; add_header Strict-Transport-Security "max-age=31536000; includeSubdomains" always; server_tokens off; # access_log /var/log/nginx/rtfm.co.ua-access.log main_ext; error_log /var/log/nginx/rtfm.co.ua-error.log warn; ssl_certificate /etc/letsencrypt/live/rtfm.co.ua/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/rtfm.co.ua/privkey.pem; ssl_protocols TLSv1.2; ssl_prefer_server_ciphers on; ssl_dhparam /etc/nginx/dhparams.pem; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_stapling on; ssl_stapling_verify on; error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } client_max_body_size 1024m; location ~ /\.ht { deny all; } location ~* \.(jpg|swf|jpeg|gif|png|css|js|ico)$ { root /data/www/rtfm/rtfm.co.ua; expires 24h; } location /wp-admin/admin-ajax.php { location ~ \.php$ { include /etc/nginx/fastcgi_params; fastcgi_pass unix:/var/run/rtfm.co.ua-php-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } } location /wp-admin/ { index index.php, index.html; auth_basic_user_file /data/www/rtfm/.htpasswd_rtfm; auth_basic "Password-protected Area"; # office allow 194.***.***.24/29; # home 397 LocalNet allow 31.***.***.117/32; # home 397 Lanet allow 176.***.***.237; deny all; location ~ \.php$ { include /etc/nginx/fastcgi_params; fastcgi_pass unix:/var/run/rtfm.co.ua-php-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } } location /wp-config.php { deny all; } location /.user.ini { deny all; } location /wp-login.php { auth_basic_user_file /data/www/rtfm/.htpasswd_rtfm; auth_basic "Password-protected Area"; # office allow 194.***.***.24/29; # home 397 LocalNet allow 31.***.***.117/32; # home 397 Lanet allow 176.***.***.237; deny all; location ~ \.php$ { include /etc/nginx/fastcgi_params; fastcgi_pass unix:/var/run/rtfm.co.ua-php-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } } location /uploads/noindex { auth_basic_user_file /data/www/rtfm/.htpasswd_rtfm; auth_basic "Password-protected Area"; } location = /favicon.ico { access_log off; log_not_found off; } location / { try_files $uri =404; index index.php; proxy_read_timeout 3000; rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.xml$ "/index.php?xml_sitemap=params=$2" last; rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.xml\.gz$ "/index.php?xml_sitemap=params=$2;zip=true" last; rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.html$ "/index.php?xml_sitemap=params=$2;html=true" last; rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.html.gz$ "/index.php?xml_sitemap=params=$2;html=true;zip=true" last; if (!-f $request_filename){ set $rule_1 1$rule_1; } if (!-d $request_filename){ set $rule_1 2$rule_1; } if ($rule_1 = "21"){ rewrite /. /index.php last; } } location ~ \.php$ { try_files $uri =404; proxy_read_timeout 3000; include /etc/nginx/fastcgi_params; fastcgi_pass unix:/var/run/rtfm.co.ua-php-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location /nginx_status { stub_status on; access_log off; allow 127.0.0.1; deny all; } }
Проверяем, запускаем (выключали, пока SSL получали):
Проверяем.
На рабочей машине добавляем в /etc/hosts
:
139.59.205.180 rtfm.co.ua
Пробуем открыть:
Отлично — SSL есть, NGINX работает.
И быстро отключаем, потому что в этот момент пишу этот пост на старом сервере 🙂
PHP-FPM
Аналогично — копируем конфиг со старого сервера.
Используем FPM-пулы, каждый под своим юзером.
См. PHP-FPM: Process Manager — dynamic vs ondemand vs static.
Linux: non-login user
Добавляем non-login юзера:
Создаём конфиг /etc/php/7.3/fpm/pool.d/rtfm.co.ua.conf
:
[rtfm.co.ua] user = rtfm group = rtfm listen = /var/run/rtfm.co.ua-php-fpm.sock listen.owner = www-data listen.group = www-data pm = dynamic pm.max_children = 5 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 ;pm.process_idle_timeout = 10s; ;pm.max_requests = 500 catch_workers_output = yes chdir = / pm.status_path = /status slowlog = /var/log/nginx/rtfm.co.ua-slow.log php_flag[display_errors] = off ;php_admin_value[display_errors] = 'stderr' php_admin_value[display_errors] = off php_admin_value[error_log] = /var/log/nginx/rtfm.co.ua-php-error.log php_admin_flag[log_errors] = on php_admin_value[session.save_path] = /var/lib/php/session/rtfm php_value[session.save_handler] = files php_value[session.save_path] = /var/lib/php/session php_admin_value[upload_max_filesize] = 128M php_admin_value[post_max_size] = 128M
Проверяем синтаксис:
Перечитываем конфиги:
Рут в NIGNX у нас:
... root /data/www/rtfm/rtfm.co.ua; ...
Создаём каталог:
Создём тестовый файл с вызовом phpinfo()
Проверяем (снова через обновление локального /etc/hosts
):
Отлично — всё работает.
MySQL
На Debian уже достаточно давно используется MariaDB вместо MySQL, но разницы особой нет. И в целом MariaDB показывает себя в тестах лучше, где-то делал сравнение.
Запускаем первичную настройку:
Создаём базу для RTFM:
Пользователя rtfm с доступом к базе rtfm_db1_production только с localhost с паролем password:
Проверяем:
Всё готово к миграции.
Миграция блога
Тут мне придётся сделать паузу на время создание дампа и перенос файлов.
После переноса — продолжу писать уже с нового сервера.
Что делаем:
- создаём архив файлов
- создаём дамп базы
- переносим их на новый сервер
- меняем запись на DNS
Сохраняем этот пост в черновиках — он будет добавлен в базу данных, которую сдамплю и перенесу, а потом продолжу с этого места.
Архив файлов
Готовим архив с файлами:
Создаём архив со сжатием:
Проверяем:
Дамп базы MySQL
Создаём дамп базы (прочтите сначала WordPress: Error establishing a database connection про опцию -d
) :
Проверяем его:
В фаерволе старого сервера разрешаем SSH с нового, и копируем данные со старого на новый:
Распаковываем файлы:
Проверяем:
Переносим каталог rtfm.co.ua
в /data/www/rtfm
:
Проверяем:
Загружаем дамп базы в новую базу:
Обновляем локальный /etc/hosts
— и:
WTF?
WordPress: Error establishing a database connection
Проверяем данные в базе — вроде всё на месте:
PHP: проверить подключение к MySQL
Пробуем РНР-скрипт:
<?php $link = @mysqli_connect('localhost', 'rtfm', 'Ta6paidie7Ie'); if(!$link) { die("Failed to connect to the server: " . mysqli_connect_error()); } else { echo "Connected\n"; } if(!@mysqli_select_db($link, 'rtfm_db1_production')) { die("Failed to connect to the database: " . mysqli_error($link)); } else { echo "DB found\n"; } ?>
Запускаем:
Тоже всё хорошо.
WordPress: WP_ALLOW_REPAIR
Копируем со старого сервера .htpasswd_rtfm
, пробуем выполнить repair
— в wp-config.php
перед строкой «‘That’s all, stop editing! Happy blogging’» добавлем:
define('WP_ALLOW_REPAIR', true);
И открываем https://rtfm.co.ua/wp-admin/maint/repair.php:
Вроде бы всё ОК — но нет:
Последним вариантом проверки было засетапить голый WordPress, который завёлся без проблем.
Понятно было, что проблема в данных в самом дампе — но какая?
Причина «Error establishing a database connection»
Пошёл я посмотреть mysqldump
-d , --no-data |
Do not write any table row information (that is, do not dump table contents). This is useful if you want to dump only the |
😀
С какого перепугу я добавил -d
при создании дампа — не знаю. Аукнулась работа с миграцией даных в AWS Database Migration Service, где надо было создать дамп чисто схемы, без самих данных.
Собственно — пересоздаём дамп уже без -d
:
Повторяем все операции, и всё завелось — продолжаю писать уже с нового сервера:
Что дальше?
Надо настроить certbot
на валидацию через webroot, добавить его в крон для автообновления сертификатов.
И закончить со оставшимися сервисами:
- amplify agent
- backup script
- logz.io
- unattended-upgrades
- msmtp
SSL: webroot validation
У нас уже есть сертификат, но мы делали валидацию через DNS.
Она вроде не работает при renew
, поэтому перенастроим на валидацию через webroot.
Let’s Encrypt: webroot валидация
Вызываем certbot
для домена rtfm.co.ua, передаём --webroot-path
вместо dns
— он должен увидеть имеющийся сертификат, и предложить сохранить, или сгенерировать новый.
На первый вопрос «How would you like to authenticate with the ACME CA?» отвечаем «Place files in webroot directory (webroot)«, на второй — «You have an existing certificate […]» — «Renew & replace the cert (limit ~5 per 7 days)«, что бы сгенерировался новый конфиг для Let’s Ecnrypt:
Да, всё хорошо — теперь проверим конфиг, который будет использоваться при обновлении сертификата:
Отлично — можно добавлять cronjob
.
certbot renew
— автообновление
Добавляем в cron задачу — раз в неделю выполнять certbot renew
.
Редактируем crontab
:
Добавляем:
@weekly certbot renew &> /var/log/letsencrypt/letsencrypt.log
Let’s Encrypt hook — NGINX reload
Последний шаг — релоадить NGINX после обновления сертификата.
Можно было ,s добавить в cron, например так:
@weekly certbot renew &> /var/log/letsencrypt/letsencrypt.log && service nginx reload
Но тогда если один из сертификатов не обновится по какой-то причине — NGINX не применит изменения вообще.
Поэтому делаем более правильно — добавляем хук в конфиг нашего домена — /etc/letsencrypt/renewal/rtfm.co.ua.conf
.
В блоке renewalparams
добавляем renew_hook
, теперь он выглядит так:
[renewalparams] account = 868c8164304408984fefbbff845d4f48 authenticator = webroot server = https://acme-v02.api.letsencrypt.org/directory webroot_path = /data/www/rtfm/rtfm.co.ua/.well-known, renew_hook = systemctl reload nginx
Собственно, с SSL мы закончили.
Amplify — мониторинг
Достаточно базовый мониторинг — но удобен, и сетапится в несколько минут. См. NGINX: Amplify — SaaS мониторинг от NGINX.
Официальная документация —
Загружаем установочный скрипт:
Передаём API ключ в переменной и запускаем скрипт:
Пару минут на установку — и новый хост появляется в дашборде:
Интереса ради — нагрузка на старом сервере после переключения rtfm.co.ua на новый сервер:
Backup скрипт для сайтов
Использую самописный скрипт, который делался ещё 3 года тому —
Вообще, для WordPress куча плагинов бекапа, которые умеют сразу и в AWS-корзины загружать — но пока тоже руки не доходят их потрогать, поэтому сделаю по старинке.
Клонируем его:
Интересно — работает ли та копия, которая в репозиториии…
Точно помню, что когда-то поломалась загрузка архивов в AWS S3, и я её так и не починил. Может, в отпуске дойдут руки.
Пока пробуем, как есть:
Ну, вроде да, работает.
Для работы скрипта нужен каталог /backups
, который монтируется отдельным диском, и файл настроек.
Добавим сначала диск.
Диски и разделы на сервере сейчас:
DigitalOcean Volume
Переходим в DigitalOcean, создаём Volume:
Проверяем:
Linux: монтирование диска
DigitalOcean Volume по-умолчанию монтируется в /mnt/rtfm_do_production_d10_backups
, и не создаёт запись в fstab
:
Отмонтируем его:
Создаём каталог /backups
:
Получаем UUID диска:
Обновляем /etc/fstab
— добавляем монтирование этого диска в /backups
, в opts с помощью nofail
указываем, что его монтирование не обязательно, что бы система могла загрузиться, если этого диска не будет:
# /etc/fstab: static file system information. UUID=4e8b8101-6a06-429a-aaca-0ccd7ff14aa1 / ext4 errors=remount-ro 0 1 UUID=a6e27193-4079-4d9d-812e-6ba29c702b75 /backups ext4 nofail 0 0
Пробуем смонтировать все разделы, указанные в /etc/fstab
:
Проверяем:
Вроде всё ок, и данные на месте:
Ещё можно перезагрузить машину, что бы убедиться, что всё работает — но это попозже, когда допишу пост.
Со старого сервера копируем конфиг для simple-backup
, и пробуем его запустить:
Вау!
И даже загрузка в AWS S3 снова работает!
Круто. Окей — с этим тоже закончили.
Что там дальше?
- logz.io
- unattended-upgrades
- logrotate
- msmtp
Logz.io, Filebeat и логи NGINX
Добавим сбор логов NGINX и отправку в Logz.io.
Регистрируем аккаунт, переходим к инструкции по сбору логов NGINX —
Надо установить
Получаем публичный сертификат для Logz.io:
Настраиваем Filebeat.
Бекапим конфиг:
Редактируем его, обновляем, как сказано в документации:
... - type: log paths: - /var/log/nginx/access.log - /var/log/nginx/rtfm.co.ua-access.log fields: logzio_codec: plain token: JzR***ZmW type: nginx_access fields_under_root: true encoding: utf-8 ignore_older: 3h - type: log paths: - /var/log/nginx/error.log - /var/log/nginx/rtfm.co.ua-error.log fields: logzio_codec: plain token: JzR***ZmW type: nginx_error fields_under_root: true encoding: utf-8 ignore_older: 3h ...
И outputs
комментируем output.elasticsearch
, и описываем output.logstash
, как сказано в документации:
... # ------------------------------ Logstash Output ------------------------------- # ... output.logstash: hosts: ["listener.logz.io:5015"] ssl: certificate_authorities: ['/etc/pki/tls/certs/COMODORSADomainValidationSecureServerCA.crt' ...
Проверяем синтаксис конфига:
И подключение к Logz.io:
Перезапускаем его:
Проверяем логи:
Данные пошли.
Остались unattended-upgrades
и logrotate
.
Установка unattended-upgrades
Описывалось в Debian: автоматические обновления с помощью unattended-upgrades и отправка почты через AWS SES, настроим на новом сервере, только без AWS SES, наверно.
Официальная документация —
unattended-upgrades
и apt-listchanges
мы уже установили, осталось настроить обновления.
Вызываем dpkg-reconfigure unattended-upgrades
:
Отвечаем Yes.
Проверим /etc/apt/apt.conf.d/20auto-upgrades
:
APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::Enable
для включения апдейтов уже не требуется, достаточно этих двух строк.
Проверим /etc/apt/apt.conf.d/50unattended-upgrades
.
В целом, там всё можно оставить по-умолчанию, но стоит добавить:
Unattended-Upgrade::Mail
— получать письма об установленных апдейтахUnattended-Upgrade::Automatic-Reboot
— на своё усмотрение, пока можно оставить в false, включить позжеUnattended-Upgrade::Automatic-Reboot-Time
— если включаем автоматическую перезагрузку — лучше ребутаться под утро, часов в 5-6 — я просыпаюсь в 7-8, если что — с утра сразу увижу, что сервер не поднялся
Запускаем пробный апгрейд:
Окей.
Теперь глянем настройки logrotate
.
logrotate
Собственно, тут уже всё настроено до нас, но проверить надо.
Все конфиги logrotate
:
Ротация логов NGINX:
Возможно, тут надо будет добавить параметр size
Проверяем работу:
Некоторые логи уже должны будут ротейтнуться, но для NGINX пока рано.
mailx
и msmtp
— отправка почты с сервера
Пользователю root будут сыпаться всякие служебные письма — хотелось бы их получать на свой почтовый ящик.
Для начала — проверяем /etc/aliases
:
Если делали изменения в этом конфиге — выполняем:
550 001.RDNS/PTR error. Rejected
Письма для root пойдут на root@example.com, но если сейчас на него отправить почту — письмо не уйдёт:
Потому что идёт через MTA Exim. Его лог:
«550 001.RDNS/PTR error. Rejected» — потому что не настроена PTR-запись для FloatingIP, и у DigitalOcean мы сами нормально ей управлять не можем.
Потому — ставим msmtp
Раньше был ssmtp
, он уже депрекейтнулся, потому используем msmtp
.
msmtp-mta
создаст симлинк /usr/sbin/sendmail
— когда mailx
будет пытаться отправить почту через sendmail
, который является дефолтным MTA — он использует msmtp
:
Настраиваем /etc/msmtprc
:
defaults port 25 tls on tls_trust_file /etc/ssl/certs/ca-certificates.crt account freehost host freemail.freehost.com.ua from user@example.com auth on user user@example.com password password # Set a default account account default : freehost
Проверяем:
mailx: cannot send message: process exited with a non-zero status
Что бы отправлять почту через mailx
с помощью msmtp
— устанавливаем bsd-mailx
вместо mailutils
:
Иначе будем получать ошибки «mailx: cannot send message: process exited with a non-zero status» и «msmtp: no recipients found«.
Пробуем отправку с mailx
:
Теперь письма от unattended-upgrades
должны приходить на ящик, заданный в Unattended-Upgrade::Mail
.
В целом — на этом вроде бы всё.