Часто настраиваю сервера под MODX, устал держать в памяти шаги. Обычно поднимая сервер по памяти, забудешь какую-нибудь мелочь, с которой провозишься 3 часа. Пусть это руководство будет шпаргалкой. А я, по мере использования, буду его дополнять.

Поехали!
apt install sudo
sudo apt update
sudo apt install -y curl
sudo apt install -y software-properties-common
sudo apt install -y python-software-properties
sudo apt install -y language-pack-ru-base
sudo echo "ru_RU.CP1251 CP1251" >>/var/lib/locales/supported.d/ru
sudo locale-gen
sudo locale-gen en_US.UTF-8

Ставим корректную локаль на сервере:
tee /etc/default/locale <<'EOF'
LANGUAGE=en_US.UTF-8
LC_ALL=en_US.UTF-8
LANG=en_US.UTF-8
LC_TYPE=en_US.UTF-8
EOF
После этого перезаходим на сервер по SSH, чтобы изменения применились.

Ставим ноду и галп:
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt-get install -y nodejs
npm install --global gulp
Тут подробнее про установку на Ubuntu.

Git, zip, cron и т.д.:
sudo apt install -y git zip unzip mc nano htop cron libyaml-dev

Добавляем новую группу юзеров sftp и правим конфиг /etc/ssh/sshd_config:
sudo addgroup sftp; \
file_ssh_config=/etc/ssh/sshd_config; \
sudo sed -ri 's/^#?\s?(Subsystem\ssftp\s.*)$/#\1/' $file_ssh_config; \
if ! grep -q "Subsystem sftp internal-sftp" $file_ssh_config; then \
    echo "" | sudo tee -a $file_ssh_config; \
    echo "Subsystem sftp internal-sftp" | sudo tee -a $file_ssh_config; \
    echo "Match Group sftp" | sudo tee -a $file_ssh_config; \
    echo "    ChrootDirectory %h" | sudo tee -a $file_ssh_config; \
    echo "    AllowTCPForwarding no" | sudo tee -a $file_ssh_config; \
    echo "    ForceCommand internal-sftp" | sudo tee -a $file_ssh_config; \
fi; \
sudo service ssh restart

Python

Устанавливаем версию выше 3.5 и менеджер модулей pip:
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt install -y python3.8
sudo apt install -y python3-pip

Делаем Python 3.8 основным:
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.5 10
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 5
sudo update-alternatives --config python3
и выбираем из списка /usr/bin/python3.8.

Зависимости для Python приложений:
sudo python3 -m pip install ipaddr
sudo python3 -m pip install pyyaml
sudo python3 -m pip install pymysql


Возможно, при установке модулей через pip, возникнет ошибка типа ImportError: No module named 'setuptools', или Command ... failed with error code, или cannot import name 'sysconfig' from 'distutils', или module 'platform' has no attribute 'linux_distribution'. Решить её можно так:
sudo apt remove python3-pip
sudo apt autoremove
sudo curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
sudo python3 get-pip.py
sudo python3 -m pip install --upgrade pip
sudo python3 -m pip install setuptools
sudo python3 -m pip install --upgrade setuptools
И снова запустить установку модуля, на котором спотыкнулись.

MySQL

Устанавливаем:
sudo apt install -y mysql-server

Для Ubuntu 20.04 с репозитория будет скачана MySQL 8.0 mysql --version. Если нужна 5.7, то сначала удаляем 8 версию:
sudo apt remove --autoremove mysql-server mysql-client && sudo rm -Rf /var/lib/mysql

Ставим репозиторий:
sudo wget https://dev.mysql.com/get/mysql-apt-config_0.8.12-1_all.deb && sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb

В процессе надо будет выбрать дистрибутив. Для Ubuntu 18.04 и выше выбираем bionic. Далее жмём MySQL Server and Cluster и выбираем нужную версию MySQL (5.7). Жмём Ok.

Обновляем репозитории:
sudo apt update

Может возникнуть ошибка Err:1 http://repo.mysql.com/apt/ubuntu bionic InRelease The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 467B942D3A79BD29.
Решается командой:
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29
sudo apt update

Смотрим какие версии доступны в репозитории:
sudo apt-cache policy mysql-server

Копируем название нужной версии (у меня это 5.7.42-1ubuntu18.04) и вставляем в команду установки, если она отличается:
sudo apt install -y mysql-client=5.7.42-1ubuntu18.04 \
mysql-community-server=5.7.42-1ubuntu18.04 \
mysql-server=5.7.42-1ubuntu18.04

Запускаем получившуюся команду.

Проблемы с MySQL

Есть вероятность, что SQL MODE будет не тот. От этого MODX будет сыпать ошибки в собственный лог и ТВ не будут сохраняться. Наверняка последствий больше… лечится это так: sudo touch /etc/mysql/conf.d/mysqld.cnf && sudo mcedit /etc/mysql/conf.d/mysqld.cnf и в конец дописываем:
[mysqld]
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

Командуем:
sudo systemctl restart mysql

Если после этого возникает ошибка с sql_mode=only_full_group_by, то скорее всего AppArmor не даёт применить настройки MySQL. Надо прописать в его конфиг /etc/apparmor.d/local/usr.sbin.mysqld в самый конец:
/etc/mysql/** r,
/etc/alternatives/** r,

После чего командуем:
service apparmor restart
sudo systemctl restart mysql

Если конфиг MySQL до сих пор не применяется, то командуем mysql и смотрим, пишет ли он какие-то предупреждения, похожие на [Warning] World-writable config file '/etc/mysql/my.cnf' is ignored. Исправляем так:
chmod 0775 /etc/mysql/my.cnf

Если периодически MODX на посещаемом сайте наполняет лог чем-то типа Error HY000 executing statement ... errno: 24 - Too many open files, то надо увеличить кол-во доступных файловых дескрипторов. Для начала выполняем команду где-то в adminer или phpmyadmin SHOW VARIABLES LIKE 'open_files_limit' и смотрим, сколько их доступно MySQL на данный момент. После этого прописываем в /etc/mysql/conf.d/mysqld.cnf заведомо большее значение в виде:
[mysqld]
open_files_limit=32768

Однако этого может быть недостаточно, поэтому командуем:
ulimit -n 32768 && ulimit -a | grep "open files"

Но и это может не повлиять на переменную open_files_limit в MySQL, в частности на системах с systemd. Поэтому в /etc/systemd/system/multi-user.target.wants/mysql.service в секцию [Service] прописываем:
LimitNOFILE=32768

После этого командуем:
sudo systemctl daemon-reload 
sudo systemctl restart mysql

Nginx, PHP

Ставим репозиторий новых версий PHP:
sudo add-apt-repository ppa:ondrej/php
sudo apt update

Возможно, возникнет ошибка при попытке обновить список пакетов командой apt update, вроде этой:
Err:4 http://ppa.launchpad.net/ondrej/php/ubuntu xenial InRelease The following signatures couldn't be verified because the public key is not available: NO_PUBKEY [...]
Вместо 3 точек будет публичный ключ, копируем его и вставляем в конец команды, вместо трёх точек:
sudo apt-key adv --recv-keys --keyserver keyserver.ubuntu.com ...
После этого снова apt update.

Устанавливаем:
sudo apt install -y nginx \
&& sudo apt install -y php7.0-fpm php7.0-mcrypt php7.0-mysql php7.0-curl php-db php7.0-gd php7.0-xml php7.0-json php7.0-imap php7.0-dev php7.0-zip php7.0-yaml php7.0-mbstring \
&& sudo apt install -y php7.1-fpm php7.1-mcrypt php7.1-mysql php7.1-curl php7.1-gd php7.1-xml php7.1-json php7.1-imap php7.1-dev php7.1-zip php7.1-yaml php7.1-mbstring \
&& sudo apt install -y php7.2-fpm php7.2-mysql php7.2-curl php7.2-gd php7.2-xml php7.2-json php7.2-imap php7.2-dev php7.2-zip php7.2-yaml php7.2-mbstring \
&& sudo apt install -y php7.3-fpm php7.3-mysql php7.3-curl php7.3-gd php7.3-xml php7.3-json php7.3-imap php7.3-dev php7.3-zip php7.3-yaml php7.3-mbstring \
&& sudo apt install -y php7.4-fpm php7.4-mysql php7.4-curl php7.4-gd php7.4-xml php7.4-json php7.4-imap php7.4-dev php7.4-zip php7.4-yaml php7.4-mbstring

Настраиваем:
sudo apt update && sudo apt upgrade
sudo service php7.0-fpm restart && sudo service php7.1-fpm restart && sudo service php7.2-fpm restart && sudo service php7.3-fpm restart && sudo service php7.4-fpm restart
sudo nginx -t
sudo service nginx reload
sudo mkdir /etc/nginx/conf.inc && sudo mkdir /etc/nginx/conf.inc/main && sudo mkdir /etc/nginx/conf.inc/access && sudo mkdir /etc/nginx/conf.inc/domains && sudo touch /etc/nginx/blockips.conf

Возможно, придётся установить YAML иначе:
sudo apt install -y php-pear
sudo pecl install yaml

Если нам нужен PHP 5.6:
sudo apt install -y php5.6-fpm php5.6-mcrypt php5.6-mysql php5.6-curl php5.6-gd php5.6-xml php5.6-json php5.6-imap php5.6-dev php5.6-zip php5.6-yaml php5.6-mbstring

В файле /etc/nginx/nginx.conf заменить всё на:
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 768;
    #multi_accept on;
}

http {
    # >> IP blocks
    include blockips.conf;
    # << IP blocks

    # >> Basic Settings
    merge_slashes off;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    client_max_body_size 100M;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    # << Basic Settings

    # >> SSL Settings
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;
    # << SSL Settings

    # >> Logging Settings
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
    # << Logging Settings

    ##
    # Gzip Settings
    ##
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.0;
    gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/xml+rss text/javascript;

    # >> Virtual Host Configs
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
    # << Virtual Host Configs
}

Командуем:
sudo nginx -t
sudo service nginx reload

В файлах /etc/php/{version}/cli/php.ini и /etc/php/{version}/fpm/php.ini раскомментировать параметр date.timezone и установить значение:
php_timezone='Europe\/Moscow'; \
for version in `ls -F1 /etc/php/ | grep -e ./ | tr -d \/`; do \
    if [ -f "/etc/php/${version}/fpm/php.ini" ]; then \
        sed -ri 's/^;?(date\.timezone =).*$/\1 '${php_timezone}'/' /etc/php/$version/fpm/php.ini; \
    fi; \
    if [ -f "/etc/php/${version}/cli/php.ini" ]; then \
        sed -ri 's/^;?(date\.timezone =).*$/\1 '${php_timezone}'/' /etc/php/$version/cli/php.ini; \
    fi; \
done

Также, возможно придется добавить модуль yaml в php конфиг, если в папке ./fpm/conf.d и ./cli/conf.d нет файла 20-yaml.ini. Для этого добавим строку рядом с другими подобными:
extension=yaml.so

Командуем:
sudo service php7.0-fpm restart && sudo service php7.1-fpm restart && sudo service php7.2-fpm restart && sudo service php7.3-fpm restart && sudo service php7.4-fpm restart

Composer глобально:
curl -sS https://getcomposer.org/installer -o composer-setup.php
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer

Может потребоваться сделать в терминале по-умолчанию PHP другой версии:
sudo update-alternatives --config php

Даймон для хост-панельки

Качаем, располагаем в директории /root/scripts/py/modxpaneldaemon/, создаем папки log и tmp в директории даймона:
mkdir -p /root/scripts/py/ \
&& curl -L -o modxpaneldaemon.zip "https://github.com/gvozdb/modxPanelDaemon/archive/master.zip" \
&& unzip modxpaneldaemon.zip -d "/root/scripts/py/" \
&& mv /root/scripts/py/modxPanelDaemon-master /root/scripts/py/modxpaneldaemon \
&& rm -f modxpaneldaemon.zip \
&& mkdir /root/scripts/py/modxpaneldaemon/log/ \
&& mkdir /root/scripts/py/modxpaneldaemon/tmp/

После, настраиваем config.yaml (обязательно заменить secret_key).

Выполняем команду для создания сайта хост-панели:
/bin/bash /root/scripts/py/modxpaneldaemon/script/sh/addmodx.sh -a 7.4 -v 2.8.4-pl -p "пароль_mysql" -h h1.you_main_domain.ru -u host -d h1.you_main_domain.ru

Может понадобиться удалить сайт хост-панели:
/bin/bash /root/scripts/py/modxpaneldaemon/script/sh/remove.sh "пароль_mysql" host

Чтобы даймон стал сервисом (/etc/init.d/modxpanel start|stop|restart или service modxpanel start|stop|restart), надо создать файл /etc/init.d/modxpanel:
#!/bin/bash
### BEGIN INIT INFO
# Provides:          ModxPanel
# Required-Start:    $local_fs $network $named $time $syslog
# Required-Stop:     $local_fs $network $named $time $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Description:       Service of ModxPanelDaemon [Python].
### END INIT INFO

prog=modxpanel
RETVAL=0

start() {
    echo "Starting $prog…"
    /usr/bin/env python3 /root/scripts/py/modxpaneldaemon/modxpaneldaemon.py start
    RETVAL=$?
    return $RETVAL
}

stop() {
    echo "Stopping $prog…"
    /usr/bin/env python3 /root/scripts/py/modxpaneldaemon/modxpaneldaemon.py stop
    RETVAL=$?
    return $RETVAL
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        stop
        start
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}"
        exit 1
    ;;
esac

exit 0

Сделать его исполняемым:
sudo chmod +x /etc/init.d/modxpanel

Добавить в автозагрузку:
update-rc.d "modxpanel" defaults

И запустить сервис:
/etc/init.d/modxpanel start

Хост-панель

Качаем и устанавливаем в систему сайта h1.you_main_domain.ru.
В системных настройках обязательно указать secret_key даймона, порт и другое.
Теперь можно создавать MODX сайты через неё!

Блокируем попытки брутфорса

Устанавливаем iptables:
apt install iptables-persistent

Вставляем два правила командами в консоли:
iptables -I INPUT -p tcp --dport 22 -i eth0 -m state --state NEW -m recent --set
iptables -I INPUT -p tcp --dport 22 -i eth0 -m state --state NEW -m recent  --update --seconds 600 --hitcount 4 -j DROP
Если в течение 10 минут будет 4 неуспешные попытки входа по SSH, то заблокировать данный IP.

Сохраняем правила:
iptables-save > /etc/iptables/rules.v4

Проверяем:
iptables -nvL

Также имеет смысл сменить порт SSH. Для этого в /etc/ssh/sshd_config заменяем Port 22 на уникальный порт в диапазоне 2001-65535. И перезапускаем службу:
service ssh restart

Короткая команда chmod

В корневой директории каждого юзера есть файл chmod, который ставит права на файлы и папки. Периодически, после ковыряния рутом надо запускать его, чтобы не было проблем с правами. Можно запускать вручную так:
sh /var/www/{username}/chmod
А можно сократить до безобразия. Для этого идём в /root/.bashrc и в конец добавляем:
# Short run chmod for user
ch() {
    sh /var/www/$1/chmod
}
Теперь запустить сброс прав на файлы и папки куда быстрее:
ch {username}

Короткая команда NGINX reload

Часто при настройке NGINX конфига приходится командовать nginx -t и если ошибок нет, то service nginx reload. Это можно делать на автомате. Для этого идём в /root/.bashrc и в конец добавляем:
# Reload NGINX or print failure message
ngxreload() {
    NGINX_TEST=`nginx -t 2>&1`; if [[ $NGINX_TEST =~ .*failed.* ]]; then echo "$NGINX_TEST"; else service nginx reload && echo "OK"; fi
}
nginxreload() {
    ngxreload
}
Теперь командуя nginxreload или ngxreload сначала будет происходить тест и только потом релоад конфига, иначе вывод ошибки, если нарушен синтаксис.

Короткая команда получения размера папки

В /root/.bashrc в конец добавляем:
# Folder size
foldersize() {
    du -chs $1
}
dirsize() {
    foldersize $1
}
Проверить размер директории так:
dirsize /var/www

Docker

Ставим Docker:
cd /tmp
wget -qO- https://get.docker.com/ | sh

Проверяем, что Docker установился:
sudo docker run hello-world

Sprut.io

Ставим Sprut.io — sprut.io/ru/install

Бекапы с Gvozdb/Dumper

Здесь описано подробно, как настраивать и пользоваться библиотекой.

SSL сертификаты

Здесь подробно про SSL сертификаты от Let's encrypt.