История роста: сначала на VPS сел один сайт, потом второй, пятый, и вот уже десять проектов с разными владельцами, а конфигурация Nginx одна на всех. Журналы свалены в общий access.log, сертификаты выпущены одной пачкой, PHP обрабатывает все сайты из общего пула под одним пользователем. В таком хозяйстве просроченный сертификат валит сразу все проекты, а один зацикленный скрипт тормозит остальных. Хорошая новость: Nginx с рождения умеет держать десятки доменов раздельно, свой server блок на сайт, свой каталог журналов, свой сертификат, свой пул PHP. Ниже рабочая схема хозяйства от структуры каталогов до автопродления сертификатов.
Механика server blocks Nginx при десятке доменов на одном адресе
За модным названием прячется простая логика. Nginx читает заголовок Host из запроса и ищет среди server блоков тот, чей server_name совпадает точнее всего. Порядок строгий: точное имя, затем самый длинный шаблон со звёздочкой в начале, затем со звёздочкой в конце, и лишь потом регулярные выражения по порядку объявления. Блок с пометкой default_server в директиве listen принимает всё несовпавшее: запросы по голому IP, кривые имена, следы сканеров.
На защищённых соединениях работает тот же принцип плюс SNI: при рукопожатии клиент называет домен, сервер в ответ подставляет сертификат именно этого сайта. Один адрес держит десять разных сертификатов без конфликтов. Клиентов без SNI почти не осталось, но их запросы получат документ блока default_server, поэтому под пометкой ставят главный домен.
Минимальная пара блоков выглядит так:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php index.html;
}
server {
listen 80;
server_name example.net;
root /var/www/example.net/public;
index index.html;
}
Отдельно про HTTP/2: начиная с версии 1.25.1 протокол включается отдельной строкой http2 on внутри блока, прежняя запись listen 443 ssl http2 считается устаревшей. Свежие ветки это давно умеют: основная линия разработки дошла до 1.31.6, стабильная до 1.30.5.
Структура каталогов и файлов конфигурации для десяти сайтов
Порядок в файлах экономит часы, когда сайтов больше трёх. Схема знакома каждому, кто видел Debian: каталог sites-available хранит все конфигурации, sites-enabled содержит только симлинки включённых сайтов, включение равносильно созданию ссылки.
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
http {
include /etc/nginx/sites-enabled/*.conf;
}
Дисциплина именования простая: файл называется по основному домену, example.com.conf, дополнительные имена перечислены внутри в директиве server_name. Один файл равен одному сайту, никаких "общих" конфигураций на три домена, иначе через полгода в них не разобраться. Итоговая структура выглядит аккуратно:
/etc/nginx
nginx.conf
sites-available
example.com.conf
example.net.conf
sites-enabled
example.com.conf -> ../sites-available/example.com.conf
example.net.conf -> ../sites-available/example.net.conf
/var/www
example.com
public
example.net
public
Включение нового сайта и применение правок занимают пару строк:
ln -s /etc/nginx/sites-available/example.net.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
Удаление сайта сводится к удалению симлинка, оригинал остаётся на случай возвращения. Общие настройки уровня http: сжатие gzip, списки mime-типов, параметры TLS и формат журналов, живут в nginx.conf один раз на всех, а специфичное для домена лежит в его личном файле. Сравнение файлов из sites-available показывает разницу между сайтами, перенос настроек делается построчно.
Раздельные журналы каждого сайта и ротация без потери записей
Свалка в один access.log убивает разбор инцидентов: чтобы понять, что случилось у третьего клиента, читают чужие двести тысяч строк. Схема начинается с единого формата, объявленного один раз на уровне http:
log_format extended '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" $request_time';
Дальше каждый server блок прописывает личные пути:
access_log /var/log/nginx/example.com/access.log extended;
error_log /var/log/nginx/example.com/error.log warn;
Каталог создаётся на сайт с правами 0750: владелец root, группа равна пользователю клиента. Клиент читает собственные журналы через панель или по SFTP, в чужие не заглядывает.
mkdir -p /var/log/nginx/example.com
chown root:example /var/log/nginx/example.com
chmod 0750 /var/log/nginx/example.com
Нюанс: без явного access_log внутри блока запросы падают в общий журнал уровня http, и каша возвращается за вечер. Проверить итоговое состояние всех блоков помогает команда nginx -T, она печатает собранную конфигурацию целиком.
Ротацию уводят в собственный шаблон logrotate с маской по подкаталогам:
/var/log/nginx/*/*/*.log {
daily
rotate 30
missingok
compress
delaycompress
sharedscripts
postrotate
[ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript
}
Сигнал USR1 заставляет мастер-процесс переоткрыть файлы: записи не теряются, служба не останавливается. Тридцать суточных копий с компрессией занимают копейки, зато спор о посещаемости двухнедельной давности решается за минуту. Колонка request_time в общем формате даёт ещё один плюс: медленный сайт вычисляется по журналу.
Отдельный SSL сертификат на каждый домен и автопродление без простоя
Соблазн выпустить один сертификат на все домены велик, но дисциплина важнее. Один общий документ означает общий срок годности: истекает в среду ночью, и одновременно останавливаются все десять сайтов. Отдельный сертификат на домен изолирует риск: перевыпуск одного документа не трогает остальные девять.
Выпуск для сайта с установленным плагином Nginx выглядит так:
certbot --nginx -d example.com -d www.example.com
Certbot сам находит нужный блок, подтверждает права временным запросом, вписывает ssl_certificate и ssl_certificate_key, добавляет перенаправление с 80 порта и перезагружает службу. Сертификаты ложатся в персональный подкаталог letsencrypt, и в заголовке блока появляются строки вида:
listen 443 ssl;
http2 on;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
Для конфигураций, собранных вручную, годится режим certonly с корнем сайта, он только выпускает документ и не трогает конфиг:
certbot certonly --webroot -w /var/www/example.com/public -d example.com -d www.example.com
Автопродление в нормальной установке уже настроено: таймер certbot.timer проверяет сроки дважды в сутки и обновляет документы, которым осталось меньше тридцати дней. Свежий выпуск утилиты имеет номер 5.8.0. Полезные привычки: контрольный прогон против тестового окружения заранее и скрипт перечитывания конфигурации после успешного обновления:
certbot renew --dry-run
#!/bin/sh
systemctl reload nginx
Файл скрипта кладут в /etc/letsencrypt/renewal-hooks/deploy/ и дают право запуска. Reload, в отличие от рестарта, не рвёт соединения: мастер-процесс перечитывает конфигурацию, старые рабочие добивают начатые запросы, новые приходят уже с обновлёнными сертификатами. Проверочные выпуски против тестового окружения не расходуют лимиты реальных выдач, для десяти разных доменов запас большой.
Раздельные пулы PHP для каждого сайта и изоляция прав на файлы
Самая дорогая привычка многоклиентского хостинга: все сайты работают одним пулом PHP-FPM от одного пользователя. Стоит одному клиенту поставить устаревший плагин, и файлы соседей, включая конфиги с паролями, становятся читаемыми для чужого кода. Правильная схема повторяется по кругу: один системный пользователь на сайт, один пул на пользователя, один сокет на пул.
useradd -r -d /var/www/example.com -s /usr/sbin/nologin example
mkdir -p /var/www/example.com/public /run/php
chown -R example:example /var/www/example.com
Пул описывается коротким файлом в pool.d:
[example.com]
user = example
group = example
listen = /run/php/example.com.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 4
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 500
slowlog = /var/log/nginx/example.com/php-slow.log
request_slowlog_timeout = 3s
php_admin_value[open_basedir] = /var/www/example.com:/tmp
php_admin_value[session.save_path] = /var/www/example.com/sessions
Nginx передаёт запросы на личный сокет:
location ~ \.php$ {
include fastcgi.conf;
fastcgi_pass unix:/run/php/example.com.sock;
}
Арифметика памяти решает, сколько сайтов выдержит сервер. Дочерний процесс PHP в типичном проекте на CMS занимает от 50 до 80 МБ. При четырёх гигабайтах оперативной памяти и десяти пулах по четыре процесса средний расход считается на салфетке: 10 умножить на 4, умножить на 65, около 2,6 ГБ. Остаётся примерно гигабайт на Nginx, систему и файловый кэш. Тяжёлым проектам пул урезают до двух-трёх процессов и добавляют кэширование страниц.
Память кэша opcache достаётся всем пулам сразу, они живут внутри одного мастер-процесса PHP-FPM, поэтому opcache.memory_consumption в php.ini считают на всё хозяйство. Пул со своим pm.max_children не даст одному проекту расплодить сотню дочерних процессов и съесть память. Личный slowlog мгновенно показывает, какой именно сайт тормозит. Сессии разнесены по каталогам, open_basedir запирает PHP в директории проекта.
Лимиты соединений и запросов, чтобы один сайт не мешал соседям
Десять сайтов делят процессор, память и полосу: без лимитов один зацикленный скрипт занимает все рабочие процессы и укладывает соседей. Nginx решает через зоны ограничений. Зоны объявляются на уровне http и получают имя, память для отслеживания адресов и скорость:
limit_req_zone $binary_remote_addr zone=req_example:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_example:10m;
В server блоке конкретного сайта зона включается с запасом на всплески:
limit_req zone=req_example burst=20 nodelay;
limit_conn conn_example 20;
client_max_body_size 32m;
Параметр burst задаёт очередь всплеска: посетитель с двадцатью картинками на странице не получит отказ, а монотонный поток ударов упрётся в потолок. Код ответа при превышении настраивается директивой limit_req_status, по умолчанию Nginx отвечает 503, популярна и честная 429. Витрине хватает пяти запросов в секунду, серверному API планка выше, форме загрузки видео выдают увеличенный client_max_body_size.
Смысл изоляции в том, что лимиты живут внутри блока сайта: агрессивный трафик на один домен упирается в собственный потолок и не парализует остальных. Нагрузочный прогон по одному домену оставит в его журнале коды 429 и 503, соседи ответят с обычной скоростью.
План обслуживания десяти сайтов и добавление нового домена
Когда структура готова, новый домен перестаёт быть событием. После первичной настройки добавить сайт удаётся за десять минут по короткому циклу:
1. завести системного пользователя, каталог кода и папку журналов;
2. скопировать шаблон блока сайта и заменить в нём домен, пути и имя сокета;
3. включить конфигурацию симлинком и проверить синтаксис командой nginx -t;
4. выпустить сертификат certbot --nginx на основной домен и его www-имя;
5. перечитать конфигурацию Nginx и убедиться по журналу, что соединения идут.
Обслуживание сводится к трём привычкам. Раз в неделю снимается свежий архив конфигураций с сертификатами:
tar -czf /root/nginx-conf-$(date +%F).tgz /etc/nginx /etc/letsencrypt
Архив хранится с правами root, внутри лежат и приватные ключи. Раз в месяц контрольный прогон certbot renew --dry-run, он же проверка таймера продления. Ежедневно минутный взгляд на свежие записи журналов ошибок всех сайтов:
tail -n 20 /var/log/nginx/*/*/error.log
Правка конфигурации одного сайта не задевает остальных: изменение, проверка nginx -t, reload без остановки соединений, соседи ничего не замечают.
Десять сайтов при такой схеме ведут себя как десять маленьких серверов: свои журналы, свои сертификаты, свои лимиты, свои пользователи. Администратор получает то, за что платит ежемесячной платой за VPS: предсказуемость и сон без ночных звонков, а проблема одного клиента остаётся его личной проблемой.