Свежий сервер Ubuntu после первой загрузки похож на пустой стол: система есть, сайтов нет. Классическая связка LEMP закрывает задачу целиком - Linux даёт систему, nginx принимает соединения, MariaDB хранит данные приложения, PHP-FPM исполняет код движка. Буква E в аббревиатуре родилась из произношения Engine X, а P давно означает отдельный менеджер процессов для PHP, а не модуль внутри веб сервера. Ниже полный путь от чистой Ubuntu Server 24.04 LTS до работающего PHP сайта с базой: установка всех пакетов из штатного репозитория, соединение nginx с PHP-FPM через unix сокет, создание базы с отдельным пользователем и виртуальный хост с конфигами, готовыми к копированию. По дороге встретятся 502, права на сокет и другие классические грабли.

Какие версии пакетов ждут в стандартном репозитории Ubuntu Server 24.04 LTS

Первый соблазн при сборке стека - подключить сторонние репозитории ради самых свежих релизов. Для классического LEMP это почти всегда лишний шаг. Архив Ubuntu 24.04 LTS уже держит согласованный набор: nginx ветки 1.24, PHP 8.3 и MariaDB ветки 10.11, в текущей сборке 10.11.13. Ветка 10.11 у MariaDB относится к выпускам с долгой поддержкой, nginx в дистрибутиве проходит тесты мейнтейнеров, а PHP 8.3 обеспечен исправлениями безопасности до конца 2027 года. Расклад упрощает жизнь: все обновления приходят одной командой apt upgrade из одного доверенного места.

Сам PHP 8.3 вышел в ноябре 2023 года и принёс типизированные константы классов, атрибут #[\Override] для строгой проверки переопределений и функцию json_validate() для быстрой проверки JSON без блока try catch. Движкам вроде WordPress, Laravel или Битрикс ветка подходит без оговорок: актуальные версии всех популярных систем на ней работают.

Если сервер всё ещё держится на Ubuntu 22.04 LTS, PHP 8.3 там не ждёт - из коробки доступен PHP 8.1. Выхода два: обновление системы до 24.04 либо известный в сообществе PPA с обновлёнными сборками PHP:

sudo add-apt-repository ppa:ondrej/php
sudo apt update

Подключать его стоит только тогда, когда ветки из архива действительно не хватает: лишний репозиторий - лишняя поверхность для сюрпризов при массовом обновлении.

Подготовка Ubuntu Server к приёму сайтов и базовая защита по SSH

До пакетов сервер полезно привести в порядок: обновить индексы, поставить накопившиеся исправления и включить файрвол. На Ubuntu Server утилита ufw живёт с выключенным состоянием по умолчанию, и порядок действий важен: сначала правила для SSH, потом активация.

sudo apt update && sudo apt full-upgrade
sudo reboot
sudo ufw allow OpenSSH
sudo ufw allow "Nginx Full"
sudo ufw enable
sudo ufw status verbose

Профиль Nginx Full открывает оба веб порта сразу: 80 под обычные запросы и 443 под будущие TLS сертификаты. Правило для OpenSSH добавляют строго до включения ufw, иначе доступ по SSH оборвётся в момент активации, и возвращаться на сервер придётся через панель хостера.

Второй шаг - автоматические обновления безопасности:

sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Критичные исправления из секции security подтягиваются по расписанию сами, и для веб стека это особенно ценно: уязвимость закрывается без ручного визита администратора, даже если он в отпуске.

Установка Nginx и проверка его службы через браузер и systemctl

sudo apt install nginx
systemctl status nginx

Служба стартует сразу после установки и попадает в автозагрузку. Быстрая проверка прямо с сервера:

curl -I http://127.0.0.1/

В ответе придёт HTTP/1.1 200 OK и заголовок Server с версией. Страница приветствия, которую открывает браузер, лежит в /var/www/html и обслуживается хостом default. Куда смотреть при правках конфигурации:

/etc/nginx/nginx.conf
/etc/nginx/sites-available/
/etc/nginx/sites-enabled/
/var/log/nginx/

Скелет главного конфига держит всю механику, эти строки трогают редко:

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    sendfile on;
    tcp_nopush on;
    types_hash_max_size 2048;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    gzip on;
    access_log /var/log/nginx/access.log;
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Директива worker_processes auto создаёт по процессу на каждое ядро, user задаёт пользователя рабочих процессов, два include в конце собирают виртуальные хосты. Каталог sites-enabled держит символические ссылки на файлы из sites-available: хосты включают и выключают ссылкой, не теряя конфигурацию.

Для сайтов с крупными загрузками в блок http или в конкретный хост добавляют лимит тела запроса, иначе nginx разрежет большие файлы ошибкой 413:

client_max_body_size 32m;

Значение должно совпадать с upload_max_filesize из PHP, о нём ниже.

Установка PHP 8.3 с PHP-FPM и расширениями под популярные движки

Ядро ставится пакетом php8.3-fpm, но сайту всегда нужен и набор расширений: драйвер MySQL, графика, архивы, кодировки. Набор ниже закрывает требования большинства движков с первого захода:

sudo apt install php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd php8.3-mbstring php8.3-xml php8.3-zip php8.3-intl php8.3-bcmath php8.3-cli php8.3-opcache

Проверка после установки:

php -v
systemctl status php8.3-fpm
ls -l /run/php/

В /run/php появится сокет php8.3-fpm.sock - точка, через которую nginx передаёт PHP запросы воркерам. Механика отличается от старого mod_php: воркеры PHP-FPM живут отдельными процессами, веб сервер лишь переправляет им запросы по протоколу FastCGI. Тяжёлый скрипт упирается в лимит своих воркеров, а статика продолжает раздаваться без задержек.

Типовые значения под CMS правят в /etc/php/8.3/fpm/php.ini:

memory_limit = 256M
upload_max_filesize = 32M
post_max_size = 32M
max_execution_time = 180

Кэш байткода opcache в Ubuntu включён сразу, ему полезно добавить памяти в том же файле:

opcache.memory_consumption = 192
opcache.interned_strings_buffer = 16
opcache.max_accelerated_tokens = 20000

После правок конфигурацию перечитывает перезапуск службы:

sudo systemctl restart php8.3-fpm

Рядом с php.ini лежит каталог pool.d с файлами пулов воркеров. В базовом сценарии хватает штатного пула www; его мощность раскрывается на серверах с несколькими сайтами, где каждому проекту выделяют собственный пул с отдельным пользователем и лимитами.

Соединение Nginx с PHP-FPM через unix сокет и безопасный location

Штатный файл default в /etc/nginx/sites-available уже содержит нужный блок, достаточно убрать комментарии:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Сниппет fastcgi-php.conf делает два дела: проверяет, что запрошенный скрипт существует, и передаёт воркеру переменную SCRIPT_FILENAME с настоящим путём до файла. Проверка существования - не формальность. Классическая атака на связку nginx с PHP построена на запросах вида /uploads/avatar.png/x.php, где исполняемым подсовывают загруженный файл с приписанным вторым расширением: без проверки интерпретатор добирался до avatar.png и исполнял его содержимое. Штатный сниппет такую лазейку закрывает. Дополнительной страховкой служит ноль в cgi.fix_pathinfo в php.ini пула: тогда интерпретатор не пытается искать скрипт по частям пути.

После правок конфигурацию проверяют и перечитывают:

sudo nginx -t
sudo systemctl reload nginx

Команда nginx -t ловит пропущенные точки с запятой и рассинхрон фигурных скобок до перезагрузки, а не после падения всего веб сервера.

Установка MariaDB и создание базы с отдельным пользователем для сайта

sudo apt install mariadb-server
sudo mysql_secure_installation

Вторая команда закрывает типовые слабости свежей базы: вход root остаётся только с localhost, анонимные пользователи и тестовая база уходят. Сборка из репозитория Ubuntu сразу предлагает root входить через unix socket - приложению такой вход недоступен в принципе, администратору же хватает sudo mariadb.

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

sudo mariadb
CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'kH9-paSS-7word-change';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Кодировка utf8mb4 вместо старого utf8 обязательна: она хранит четырёхбайтовые символы, включая редкие письменности и emoji в комментариях, и не превращает их в вопросительные знаки. Пароль из примера, разумеется, меняют на собственный.

Базовым ручкам производительности место в /etc/mysql/mariadb.conf.d/50-server.cnf:

[mysqld]
innodb_buffer_pool_size = 2G
max_connections = 100

Буфер Innodb держит горячие страницы таблиц в памяти, на сервере с 8 ГБ типовое значение около четверти всей памяти. Резервная копия делается одной строкой, без сторонних скриптов:

sudo mariadb-dump appdb | gzip > appdb-$(date +%F).sql.gz

Виртуальный хост Nginx для PHP сайта и финальная проверка стека

Каталог, тестовая страница и конфигурация хоста собираются за пару минут:

sudo mkdir -p /var/www/example.com/public
sudo chown -R www-data:www-data /var/www/example.com
echo '<?php echo "stack works: " . PHP_VERSION;' | sudo tee /var/www/example.com/public/index.php
sudo nano /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example.com/public;
    index index.php;

    access_log /var/log/nginx/example.com.access.log;
    error_log /var/log/nginx/example.com.error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~ /\.ht {
        deny all;
    }
}

Директива try_files отправляет все адреса, которых нет на диске, на index.php - так работают движки с единой точкой входа, от WordPress до самописных маршрутизаторов. Хост включают ссылкой и перечитывают конфигурацию:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

Пока DNS не указывает на сервер, проверку делают локально: домен добавляют в /etc/hosts рабочей машины или стучатся curl прямо с сервера:

curl -I http://127.0.0.1/ -H "Host: example.com"

Ответ 200 OK и строка stack works в теле страницы подтверждают связку nginx с PHP-FPM. Базу проверяет короткий скрипт:

<?php
$pdo = new PDO(
    'mysql:host=localhost;dbname=appdb;charset=utf8mb4',
    'appuser',
    'kH9-paSS-7word-change'
);
$row = $pdo->query('SELECT VERSION() AS v')->fetch();
echo 'MariaDB: ' . $row['v'] . PHP_EOL;

Когда вместо сайта браузер показывает 502 Bad Gateway, порядок диагностики такой:

  1. проверить статус обеих служб командой systemctl status nginx php8.3-fpm;
  2. сверить путь сокета в fastcgi_pass с реальным файлом из /run/php;
  3. прочитать свежие записи в /var/log/nginx/error.log и в логе пула;
  4. убедиться, что www-data читает каталог сайта и владелец сокета совпадает с ожидаемым.

Частые сценарии читаются за секунды. Ошибка 403 означает отсутствие индексного файла или прав на чтение каталога. Надпись File not found - рассинхрон root в хосте и реального пути до скрипта. Белый экран без ошибок говорит о выключенном display_errors: реальную причину ищут в логе пула.

Финальный штрих - TLS сертификаты. Пакет certbot с плагином под nginx получает и продлевает их одной командой:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Продления идут сами по таймеру systemd, а конфигурация хоста получает готовый блок с 443 портом и редиректом с 80.

Стек собран и проверен на каждом уровне: nginx разруливает адреса и отдаёт статику, PHP-FPM исполняет код движка в отдельных воркерах, MariaDB держит данные приложения за правами отдельного пользователя. Дальше начинается область роста - бэкапы по расписанию, кэш, логи и метрики, но фундамент у любого растущего проекта один и тот же, и теперь он стоит на диске и работает.