Ой, ничего не найдено!

К сожалению, по вашему запросу пока ничего нет (но это только пока!), зато вы можете подписаться на нашу замечательную email-рассылку, чтобы не пропустить самое интересное в будущем.

  • 21

Несколько сайтов на одном VPS: делим ресурсы без драки

  • 3 минуты на чтение

Часть 8 из 14 · Серия «Сервер в бою»

Сюжет, который проживает почти каждый владелец VPS. Сначала на сервере живёт один сайт. Потом появляется «ну давай и мой блог сюда же, чего серверу простаивать». Потом — сайт клиента, staging-версия, чей-то WordPress «на недельку». Через год на VPS живёт коммуналка из пяти сайтов, и заселялись они все по одному сценарию: скопировали конфиг nginx, поменяли домен, готово.

Работает? Работает. До первого из двух инцидентов. Инцидент первый, про ресурсы: блог соседа попал в подборку, поймал наплыв трафика — и уронил ваш магазин, потому что съел все PHP-воркеры. Инцидент второй, про безопасность, он хуже: в чужом WordPress нашлась дырка в плагине, злоумышленник получил исполнение кода — и внезапно читает конфиги всех сайтов на сервере, включая пароли от всех баз. Потому что в коммуналке, заселённой копипастом, все жильцы работают под одним пользователем и все двери открыты.

Оба инцидента лечатся на этапе заселения, тремя уровнями изоляции: nginx разводит запросы по сайтам, отдельные пользователи разводят права, отдельные пулы PHP-FPM разводят ресурсы. Разберём все три — а в конце проверим изоляцию руками, как настоящий взломщик.

Уровень 1. Nginx: один швейцар на все подъезды

Начнём с механики, которая отвечает на вопрос «как один сервер обслуживает пять доменов». Все домены указывают на один IP; nginx смотрит в заголовок Host входящего запроса и по директиве server_name решает, какому виртуальному хосту его отдать. Каждый сайт — отдельный файл в /etc/nginx/sites-available/, включается симлинком в sites-enabled/:

# /etc/nginx/sites-available/site1.ru
server {
    listen 443 ssl;
    server_name site1.ru www.site1.ru;

    root /var/www/site1/public;
    index index.php;

    # у каждого сайта — СВОИ логи, это важно
    access_log /var/log/nginx/site1.access.log;
    error_log  /var/log/nginx/site1.error.log;

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

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        # свой сокет — к нему вернёмся в уровне 3
        fastcgi_pass unix:/run/php/site1.sock;
    }
}
ln -s /etc/nginx/sites-available/site1.ru /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Привычка nginx -t перед каждым reload — из тех, что окупаются пожизненно: опечатка в одном конфиге валит все сайты сразу, а -t ловит её до того, как reload это сделает.

от 0 ₽ на старте
Свой сервер по цене чашки кофе в неделю
Прозрачные тарифы без сюрпризов в чеке. Платите за то, что реально используете, и масштабируйтесь, когда вырастете.
Смотреть тарифы

Два неочевидных момента, которые превращают «скопировал конфиг» в «настроил осознанно»:

Раздельные логи — не эстетика, а диагностика. Помните практикум про пять минут до причины 502? С общим на всех access.log вы будете искать проблему сайта в шуме четырёх соседей. С раздельными — сразу видно, кого долбят боты и чей трафик вырос.

Сервер по умолчанию. Вопрос на засыпку: что ответит nginx на запрос с доменом, которого нет ни в одном server_name? (А такие запросы будут: сканеры ходят по IP, чужие домены иногда ошибочно указывают на ваш адрес.) Ответ: отдаст его... первому по алфавиту виртуальному хосту. Сюрприз: ваш магазин открывается по чужому домену, со всеми последствиями для SEO и здравого смысла. Лечится явной заглушкой:

server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;   # для незнакомых доменов по https — просто отказ
    return 444;                # фирменный код nginx: закрыть соединение молча
}

Теперь незваные запросы получают тишину, а не случайный сайт.

Уровень 2. Пользователи: у каждого жильца — своя квартира

Вот здесь коммуналка по умолчанию устроена хуже всего. Стандартная схема «все файлы принадлежат www-data, PHP работает от www-data» означает: код сайта №3, будучи взломанным, имеет ровно те же права, что и код сайтов №1, №2, №4 и №5. Прочитать wp-config.php соседа с паролем от его базы? Пожалуйста. Дописать майнер в чужой index.php? Без проблем — тот же пользователь.

Правильная схема проста как квартплата: один сайт — один системный пользователь.

adduser --disabled-password --gecos "" site1
adduser --disabled-password --gecos "" site2
SSD/NVMe в комплекте
Скорость, которую видно в первом же отклике
NVMe-диски, свежее железо и честные ресурсы без «соседей», которые съедают всю мощность. Разница чувствуется с первого запроса.
Разогнать проект

Файлы каждого сайта живут в домашней папке владельца и принадлежат ему:

mkdir -p /var/www/site1/public
chown -R site1:site1 /var/www/site1

Осталась одна загвоздка: nginx (он работает от www-data) должен уметь читать статику сайтов — картинки, css. Решается членством в группе и аккуратными правами:

usermod -aG site1 www-data        # пускаем www-data в группу сайта
chmod 750 /var/www/site1          # владелец всё, группа — читать и заходить, остальные — ничего

И правило на стену: рабочие права — это 750 на каталоги и 640 на файлы (владелец читает-пишет, группа читает, остальные не видят ничего). Никогда — 777. Каждый chmod 777 в интернет-инструкции «чтобы заработало» — это предложение снять дверь с петель, потому что заедает замок. «Остальные» в нашей коммуналке — это в том числе взломанный сосед: ради защиты от него всё и затевалось.

Заодно та же логика — на этаж ниже, в MySQL: у каждого сайта — своя база и свой пользователь с правами только на неё (GRANT ALL ON site1_db.* TO 'site1'@'localhost'). Утёкший пароль от базы блога не должен открывать базу магазина. Три строчки при создании базы — и этот класс проблем закрыт.

Уровень 3. PHP-FPM пулы: отдельный счётчик на каждую квартиру

Теперь ресурсы и — снова — права, потому что пулы решают обе задачи разом. По умолчанию весь PHP на сервере исполняется одним пулом www от имени www-data: общий пользователь (см. инцидент второй) и общая очередь воркеров (см. инцидент первый — наплыв на блоге съедает воркеры магазина).

Заводим каждому сайту свой пул — файл /etc/php/8.3/fpm/pool.d/site1.conf:

[site1]
; исполняем код от пользователя сайта — ядро всей изоляции
user = site1
group = site1

; свой сокет — тот самый, что указан в nginx
listen = /run/php/site1.sock
listen.owner = www-data
listen.group = www-data

; персональный лимит воркеров — бюджет квартиры
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4

; и персональный лог медленных запросов — пригодится в следующей статье
slowlog = /var/log/php/site1.slow.log
request_slowlog_timeout = 5s
Запуск за 2 минуты
VPN, бот или пет-проект — поднимем за пару минут
VPN, Telegram-бот, своё облако или пет-проект — арендуйте сервер за пару минут и проверьте идею в деле уже сегодня.
Создать сервер

Копируем под каждый сайт, меняя имя, пользователя и сокет; systemctl restart php8.3-fpm — и коммуналка превратилась в нормальный дом. Что мы получили каждой строчкой:

user = site1 — PHP-код сайта работает от его собственного пользователя. Взломанный плагин WordPress теперь заперт в правах site1: файлы соседей для него — «Permission denied» (мы же выставили 750). Радиус поражения взлома сжался с «весь сервер» до «одна квартира».

pm.max_children = 8 — потолок одновременных PHP-процессов этого сайта. Наплыв трафика на блоге упрётся в его собственные 8 воркеров — посетители блога подождут, а магазин продолжит работать как ни в чём не бывало. Это и есть «делим ресурсы без драки»: не приоритеты и не магия, а простые персональные лимиты.

Как выбрать цифры — бюджетная арифметика: один PHP-воркер среднего сайта ест 40–80 МБ (посмотрите своё: ps aux | grep php-fpm, колонка RSS). Сумма max_children × размер_воркера по всем пулам плюс аппетиты MySQL и nginx должна помещаться в оперативку VPS — иначе однажды придёт наш старый знакомый OOM killer из первой статьи. На VPS с 4 ГБ и тремя сайтами разумный старт: важному сайту 10–12 воркеров, остальным по 4–6, и смотреть по факту. Тонкой настройке pm-параметров (и тому, что делать, когда воркеров не хватает честно) посвящена следующая статья — здесь важен принцип: лимиты назначаются по важности сайта, а не поровну.

Проверяем изоляцию руками

Настроили — не верим на слово, а проверяем. Побудем взломщиком: представим, что код site2 скомпрометирован, и попробуем от его имени дотянуться до соседа:

sudo -u site2 cat /var/www/site1/public/wp-config.php
cat: /var/www/site1/public/wp-config.php: Permission denied

Музыка. Теперь ресурсную изоляцию: устройте одному пулу нагрузку (хоть простым ab/wrk по его страницам) и смотрите в соседнем окне systemctl status php8.3-fpm и статистику пулов — воркеры нагруженного сайта упрутся в свой потолок, остальные пулы даже не заметят. И контрольный смотр всей картины:

ps aux | grep php-fpm
1 месяц бесплатно
Сервер с нуля — без боли и гугления каждой команды
Понятная панель, готовые шаблоны и поддержка, которая объясняет по-человечески. Поднимите проект, даже если SSH видите впервые.
Попробовать бесплатно

В выводе должна быть видна вся социология дома: мастер-процесс от root и группки воркеров от site1, site2, site3 — каждый под своим именем. Если все воркеры до сих пор от www-data — какой-то пул не подхватился, идём в journalctl -u php8.3-fpm выяснять.

Грабли коммунального хозяйства

Грабля Чем стреляет Профилактика
Всё от www-data Один взлом = все сайты Пользователь + пул на сайт
chmod 777 «чтобы заработало» То же, только добровольно 750/640; если «не работает» — чинить владельца, а не открывать всем
Нет default_server Ваш сайт отвечает по чужим доменам Заглушка с return 444
Общий access.log Диагностика вслепую Свои логи каждому (и не забыть их в logrotate — статья 5)
Сумма max_children не считана OOM среди ночи Бюджет: Σ(children × RAM воркера) + MySQL < RAM
Один пользователь MySQL на всё Утёк один пароль — утекли все базы База + пользователь на сайт
Правка «живого» конфига без nginx -t Опечатка кладёт все сайты разом nginx -t && reload, всегда
Сертификаты вручную по одному Просроченный SSL у сайта, о котором забыли certbot --nginx на каждый хост, авто-продление под присмотром (статья 12)

Отдельной строкой — деплой в новую схему: заливать файлы сайта надо от его пользователя (site1), а не от root, иначе владельцы файлов поплывут и начнётся вечная чехарда с chown. Как устроить деплой красиво — одной командой и без FTP — тема тринадцатой статьи.

Что в итоге

Три уровня — nginx разводит трафик, пользователи разводят права, пулы разводят ресурсы — и «коммуналка» превращается в нормальный многоквартирный дом: у каждого сайта свои стены, свой счётчик и свой почтовый ящик с логами. Взлом соседа больше не ваша проблема, его хабраэффект — тоже. Цена вопроса — по 15 минут на сайт при заселении, и заметьте: ни одна из мер не потребовала ни докера, ни панелей управления — только штатные механизмы, которые уже были в системе.

Но изоляция отвечает лишь за то, чтобы сайты не мешали друг другу. А если сайт тормозит сам по себе — в одиночестве, без соседей и наплывов? В следующей статье займёмся производительностью всерьёз: pm.max_children по науке, opcache, gzip и brotli, keepalive — разберём, какие крутилки nginx и PHP-FPM реально ускоряют сайт, а какие переписываются из статьи в статью как карго-культ. Спойлер: тот slowlog, что мы прописали в пуле сегодня, завтра станет главным свидетелем обвинения.

Хостинг Siteko

VDS с первого дня

Свой сервер Siteko под полным контролем

NVMe-диски, root-доступ и честные ресурсы без «соседей». Поднимите VPN, бота или прод-проект и масштабируйтесь без тесных рамок.

  • Root-доступ полная свобода настройки под ваш стек.
  • NVMe и свежее железо скорость заметна с первого запроса.
  • Перенос бесплатно поможем переехать без простоя.
Выбрать VDS