Выберите продукт

VDS: dev, stage и prod через systemd slice и nginx vhost

На одном VDS можно безопасно разместить dev, stage и prod, если грамотно изолировать ресурсы и окружения. Разберём, как использовать systemd slice, отдельных Unix-пользователей и конфигурацию nginx vhost, чтобы среды не конфликтовали, не отъедали ресурсы у продакшена и оставались удобными для разработки и выката.
VDS: dev, stage и prod через systemd slice и nginx vhost

На практике у многих проектов один-единственный VDS, на который хотят уместить dev, stage и prod. Это удобно по цене и администрированию, но чревато кашей из конфигов, конфликтами портов и внезапными падениями продакшена из‑за «безобидного» теста.

В этом тексте разберём, как аккуратно развести среды:

  • создать понятную структуру пользователей и директорий под dev/stage/prod;
  • ограничить ресурсы через systemd slice для каждой среды;
  • настроить отдельные сервисы приложения (например, PHP‑FPM, Node.js, Python) под каждую среду;
  • поднять изолированные nginx vhost (server {}), чтобы dev и stage не лезли в прод;
  • минимизировать влияние dev/stage на прод по CPU, RAM и I/O.

Фокус будет на типичном стеке «nginx + приложение как systemd‑сервис», но подход применим к PHP‑FPM, Node.js, Django, Go и т.д.

Архитектурный подход: как логически разделить dev/stage/prod на одном VDS

Перед тем как лезть в конфиги, определимся с моделью. Мы хотим добиться одновременно:

  • изолированных пользователей и прав (чтобы dev не трогал файлы prod);
  • изолированных системных служб (каждая среда со своим сервисом);
  • ограничения ресурсов по средам (через cgroup и systemd slice);
  • разных доменов/поддоменов и vhostов в nginx для удобной работы команды.

Упрощённая схема может выглядеть так:

/srv/myapp/
  dev/
    current/
    releases/
    shared/
  stage/
    current/
    releases/
    shared/
  prod/
    current/
    releases/
    shared/

/etc/systemd/system/
  myapp-dev.service
  myapp-stage.service
  myapp-prod.service
  myapp-dev.slice
  myapp-stage.slice
  myapp-prod.slice

/etc/nginx/sites-available/
  myapp-dev.conf
  myapp-stage.conf
  myapp-prod.conf

Сюда хорошо ложится модель dev → stage → prod:

  • dev — «песочница» для разработчиков, допускаются частые перезапуски и нагрузки, но ресурсы жёстко ограничены;
  • stage — стенд, максимально близкий к prod по конфигам и версии ПО, но с более мягкими лимитами;
  • prod — приоритет по ресурсам, минимальные изменения, строгий контроль.

Unix‑пользователи и директории проекта для разных сред

Первый базовый шаг — разделить Unix‑пользователей. Не обязательно плодить по пользователю на каждого разработчика, но логично завести по пользователю на каждую среду:

  • myapp-dev
  • myapp-stage
  • myapp-prod

Пример создания пользователей (без логина по SSH, только системный):

useradd --system --create-home --home-dir /srv/myapp/dev --shell /usr/sbin/nologin myapp-dev
useradd --system --create-home --home-dir /srv/myapp/stage --shell /usr/sbin/nologin myapp-stage
useradd --system --create-home --home-dir /srv/myapp/prod --shell /usr/sbin/nologin myapp-prod

Стандартную структуру подкаталогов создаём руками или деплоем:

mkdir -p /srv/myapp/dev/{releases,shared,current}
mkdir -p /srv/myapp/stage/{releases,shared,current}
mkdir -p /srv/myapp/prod/{releases,shared,current}

chown -R myapp-dev:myapp-dev /srv/myapp/dev
chown -R myapp-stage:myapp-stage /srv/myapp/stage
chown -R myapp-prod:myapp-prod /srv/myapp/prod

Дальше CI/CD или скрипты деплоя выкладывают новые версии в releases/ и переключают симлинк current. Это уже тема отдельного разговора, но такая структура хорошо дружит и с capistrano‑подобным деплоем, и с простым rsync.

Если проект растёт и вы понимаете, что скоро dev/stage придётся переносить на отдельные сервера, имеет смысл сразу закладывать такую структуру. Тогда миграция на отдельный VDS под прод или, наоборот, под dev/stage, будет сводиться к переносу одной среды, а не к полной переразметке сервера. Для более сложных сценариев переноса и смены доменов может пригодиться разбор в статье про аккуратную миграцию и 301‑редиректы — см. материал о миграции сайтов с учётом доменов и HTTPS.

Схема разделения сред dev, stage и prod на одном VDS через systemd slice и nginx

systemd slice: зачем он нужен для dev/stage/prod

systemd slice — это сущность systemd, отображающаяся в отдельную cgroup. Через неё можно:

  • ограничивать потребление CPU, памяти, I/O;
  • задавать приоритеты (то есть «когда всё плохо, прод важнее dev»);
  • группировать связанные сервисы (например, приложение, воркеры и cron‑задачи в одной среде).

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

Создаём три slice: dev, stage, prod

По умолчанию systemd создаёт system.slice, user.slice и т.д. Мы добавим свои:

cat > /etc/systemd/system/myapp-dev.slice << 'EOF'
[Unit]
Description=Slice for myapp DEV environment

[Slice]
# Ограничим память (пример: 1G на dev)
MemoryMax=1G
# Ограничим долю CPU (пример: 20% от всех CPU)
CPUQuota=20%%
# Немного ограничим IO (если поддерживается)
IOWeight=100
EOF

Аналогично для stage:

cat > /etc/systemd/system/myapp-stage.slice << 'EOF'
[Unit]
Description=Slice for myapp STAGE environment

[Slice]
MemoryMax=2G
CPUQuota=40%%
IOWeight=500
EOF

И продакшен:

cat > /etc/systemd/system/myapp-prod.slice << 'EOF'
[Unit]
Description=Slice for myapp PROD environment

[Slice]
# Прод может использовать почти весь сервер, но всё равно зададим лимиты
MemoryMax=8G
CPUQuota=90%%
IOWeight=1000
EOF

После создания или изменения файлов не забываем выполнить перезагрузку конфигурации systemd:

systemctl daemon-reload

Важно понимать, что настройки выше — лишь пример. На небольшом VDS лимиты должны быть соразмерны его ресурсам (например, на 4 ГБ RAM не выставляйте MemoryMax=8G). Настраивайте, отталкиваясь от общей памяти, числа ядер и приоритетов проекта.

Привязка сервисов к slice через systemd

Любой unit systemd (service, scope, socket) можно привязать к определённому slice через директиву Slice= в секции [Unit] или [Service]. Это и будет наш «мост» между логическими dev/stage/prod и реальными ограничениями cgroup.

Например, юнит для dev можно описать так (сохранить в /etc/systemd/system/myapp-dev.service):

[Unit]
Description=MyApp DEV service
After=network.target
Slice=myapp-dev.slice

[Service]
Type=simple
User=myapp-dev
Group=myapp-dev
WorkingDirectory=/srv/myapp/dev/current
ExecStart=/usr/bin/php artisan serve --host=127.0.0.1 --port=9001
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Для stage меняем пользователя, директорию и порт (файл /etc/systemd/system/myapp-stage.service):

[Unit]
Description=MyApp STAGE service
After=network.target
Slice=myapp-stage.slice

[Service]
Type=simple
User=myapp-stage
Group=myapp-stage
WorkingDirectory=/srv/myapp/stage/current
ExecStart=/usr/bin/php artisan serve --host=127.0.0.1 --port=9002
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

И для prod (файл /etc/systemd/system/myapp-prod.service):

[Unit]
Description=MyApp PROD service
After=network.target
Slice=myapp-prod.slice

[Service]
Type=simple
User=myapp-prod
Group=myapp-prod
WorkingDirectory=/srv/myapp/prod/current
ExecStart=/usr/bin/php artisan serve --host=127.0.0.1 --port=9003
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Команды для включения и запуска всех трёх сервисов:

systemctl enable myapp-dev.service
systemctl enable myapp-stage.service
systemctl enable myapp-prod.service

systemctl start myapp-dev.service
systemctl start myapp-stage.service
systemctl start myapp-prod.service

Теперь systemd будет учитывать лимиты по slice: если dev станет съедать слишком много CPU, то больше заданных 20% он не получит, и прод не просядет.

Конфигурация nginx vhost и пулов PHP-FPM для разных сред на экране администратора

Пример с PHP‑FPM: отдельные пулы и сервисы на среду

Если у вас типичный LEMP‑стек, приложение работает через PHP‑FPM. Логичный шаг: дать dev, stage и prod отдельные пулы. Можно делать это через один сервис php-fpm с разными конфигами пулов, а можно пойти дальше и завести разные systemd‑юниты (для разных версий PHP, например).

Условный пример пула для prod (фрагмент конфига пула):

[myapp-prod]
user = myapp-prod
group = myapp-prod
listen = /run/php-fpm-myapp-prod.sock
pm = dynamic
pm.max_children = 20
chdir = /srv/myapp/prod/current

Аналогично для stage:

[myapp-stage]
user = myapp-stage
group = myapp-stage
listen = /run/php-fpm-myapp-stage.sock
pm = dynamic
pm.max_children = 10
chdir = /srv/myapp/stage/current

И dev c меньшими лимитами:

[myapp-dev]
user = myapp-dev
group = myapp-dev
listen = /run/php-fpm-myapp-dev.sock
pm = dynamic
pm.max_children = 5
chdir = /srv/myapp/dev/current

Дальше эти сокеты будут использованы в nginx vhostах. Если хочется ещё жёстче разделить, можно завести отдельные служебные файлы php-fpm-myapp-dev.service и привязать их к соответствующим slice через Slice=myapp-dev.slice.

nginx vhost: три server {} для dev/stage/prod

Теперь очередь nginx. Задача nginx — принимать HTTP‑трафик, роутить его по доменам и проксировать в нужное приложение или PHP‑FPM пул. На уровне nginx мы окончательно разделяем dev/stage/prod по доменам и по backend‑портам или сокетам.

Типичный вариант доменов:

  • myapp.example.com — prod;
  • stage.myapp.example.com — stage;
  • dev.myapp.example.com или *.dev.myapp.example.com — dev.

Если вы уже задумываетесь о полноценном HTTPS на всех средах, удобно сразу продумать схему выдачи сертификатов. Для продакшна чаще всего берут проверенные коммерческие SSL-сертификаты с поддержкой нужных доменов, а dev и stage могут жить либо за VPN, либо на отдельных доменах с тестовыми сертификатами.

Пример vhost для prod

Пример конфига nginx для PHP‑FPM через Unix‑сокет (фрагмент файла, например /etc/nginx/sites-available/myapp-prod.conf):

server {
    listen 80;
    server_name myapp.example.com;

    root /srv/myapp/prod/current/public;
    index index.php index.html;

    access_log /var/log/nginx/myapp-prod-access.log;
    error_log  /var/log/nginx/myapp-prod-error.log;

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

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm-myapp-prod.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;
    }
}

Если приложение — это отдельное HTTP‑приложение (Node.js или Go) на порту 9003, достаточно заменить секцию location / на proxy_pass:

location / {
    proxy_pass http://127.0.0.1:9003;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

vhost для stage

Почти копия prod, но другой домен, логи и сокет или порт backend (файл /etc/nginx/sites-available/myapp-stage.conf):

server {
    listen 80;
    server_name stage.myapp.example.com;

    root /srv/myapp/stage/current/public;
    index index.php index.html;

    access_log /var/log/nginx/myapp-stage-access.log;
    error_log  /var/log/nginx/myapp-stage-error.log;

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

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm-myapp-stage.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;
    }
}

vhost для dev

Для dev часто используют wildcard‑домен или один общий dev‑поддомен. Начнём с одного домена dev.myapp.example.com (файл /etc/nginx/sites-available/myapp-dev.conf):

server {
    listen 80;
    server_name dev.myapp.example.com;

    root /srv/myapp/dev/current/public;
    index index.php index.html;

    access_log /var/log/nginx/myapp-dev-access.log;
    error_log  /var/log/nginx/myapp-dev-error.log;

    # Можно добавить базовую авторизацию или ограничение по IP
    # чтобы dev не был открыт всему интернету

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

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm-myapp-dev.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;
    }
}

После добавления файлов не забудьте включить сайты (если используется стандартная схема sites-available/sites-enabled):

ln -s /etc/nginx/sites-available/myapp-prod.conf /etc/nginx/sites-enabled/
ln -s /etc/nginx/sites-available/myapp-stage.conf /etc/nginx/sites-enabled/
ln -s /etc/nginx/sites-available/myapp-dev.conf /etc/nginx/sites-enabled/

nginx -t
systemctl reload nginx

Безопасность и удобство: важные мелочи при dev/stage/prod на одном VDS

Когда все три среды живут на одном сервере, дисциплина становится особенно важной. Несколько практических рекомендаций:

  • Разные переменные окружения. Не держите один и тот же файл .env для всех сред. Пусть у каждой среды будет свой .env с отдельными базами, ключами и настройками кэша.
  • Разные базы данных. Пусть это один инстанс MySQL или PostgreSQL, но три отдельных базы с отдельными пользователями и правами. Так меньше риска, что dev‑код затрёт прод‑данные.
  • Ограниченный доступ к dev/stage. Даже если это один сервер, dev и stage лучше закрыть по IP‑листу или базовой HTTP‑авторизацией.
  • Резервные копии prod. При общих ресурсах нагрузка dev может совпасть с временем бэкапа prod. Планируйте расписание cron или systemd timers с запасом.
  • Мониторинг per‑slice. Используйте systemd-cgtop, systemd-cgls, метрики Prometheus или node_exporter с cgroup‑лейблами, чтобы видеть, кто именно даёт нагрузку: dev, stage или prod.

Если вы разворачиваете staging для конкретного сайта (например, WordPress) и активно переключаете DNS между средами, может быть полезен более узкоспециализированный гайд по staging-окружениям — см. материал про staging WordPress на VDS с управлением DNS.

Диагностика и мониторинг: как понять, что slice и vhost работают как надо

После настройки важно не просто верить, а убедиться, что ресурсы действительно делятся по slice, а nginx корректно роутит трафик.

Проверка slice и сервисов

Полезные команды:

  • systemctl status myapp-dev.slice — покажет, какие units внутри slice и их состояние;
  • systemd-cgls — иерархия cgroup с нашими slice;
  • systemd-cgtop — потребление CPU и RAM по cgroup в реальном времени.

Если вы видите, что myapp-dev.slice упёрся в лимит MemoryMax, а prod в это время работает стабильно, значит архитектура делает именно то, что нужно.

Проверка nginx vhost

После перезагрузки nginx проверьте маршрутизацию через заголовок Host:

  • вызовите curl -H "Host: myapp.example.com" http://127.0.0.1/ и убедитесь, что это prod;
  • curl -H "Host: stage.myapp.example.com" http://127.0.0.1/ — stage;
  • curl -H "Host: dev.myapp.example.com" http://127.0.0.1/ — dev.

Смотрите access‑логи: каждый vhost пишет в свой логфайл, это удобно для анализа нагрузки по средам и поиска ошибок.

Типичные ошибки и как их избежать

Несколько распространённых граблей при совмещении dev/stage/prod на одном VDS:

  • Одинаковые порты backend. Запуск девелоперского сервера на том же порту, что и prod, ломает проксирование. Держите табличку портов или используйте только Unix‑сокеты.
  • Общие директории /tmp и /var/tmp без учёта прав. Приложения могут перетирать временные файлы друг друга. Хорошая практика — указывать свои директории tmp через переменные окружения или настройки языка и фреймворка.
  • Один и тот же кэш. Redis, Memcached и файловый кэш разделяйте по базам, префиксам или директориям, чтобы dev не ломал кэш prod.
  • Один slice на всё приложение. Если всё живёт в system.slice, то dev может забрать ресурсы prod. Создание отдельных slice — как раз способ этого избежать.
  • Отсутствие ограничений в dev. Разработчики могут случайно нагрузить сервер тяжёлым скриптом. Лимиты на уровне slice и пулов PHP‑FPM спасают от полного падения.

Как масштабировать подход, когда VDS становится тесным

Рано или поздно объём трафика и нагрузка вырастут настолько, что держать все среды на одном VDS станет неудобно. Но аккуратное разделение dev/stage/prod через systemd slice и nginx vhost упрощает миграцию:

  • конфиги vhost уже разделены по файлам, можно перенести prod на отдельный сервер, а dev/stage оставить как есть;
  • юнит‑файлы systemd легко копируются или шаблонизируются Ansible или Terraform‑ролями;
  • структура /srv/myapp/{dev,stage,prod} явно отражает, что куда деплоится.

То есть вы изначально строите архитектуру так, что «разнести среды по разным VDS» — это не рефакторинг всей инфраструктуры, а просто перенос одной из сред на новый хост с минимальными изменениями.

Итоги

Разделить dev, stage и prod на одном VDS можно без хаоса и риска завалить прод, если использовать инструменты, которые уже есть в современном Linux:

  • отдельные Unix‑пользователи и директории под каждую среду;
  • systemd slice для ограничения и приоритизации ресурсов;
  • отдельные systemd‑юниты для сервисов приложения;
  • чётко разделённые nginx vhost (server {}), завязанные на разные домены и backend‑сокеты или порты;
  • аккуратное разделение баз данных, кэшей и настроек окружения.

Такой подход хорошо масштабируется: сначала он дисциплинирует работу на одном VDS, а потом позволяет безболезненно разнести среды по разным серверам, когда проект вырастет.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину

Ошибка mount: wrong fs type, bad option, bad superblock в Debian/Ubuntu может означать и простую опечатку в имени раздела, и пробл ...
Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление

Если XFS-раздел внезапно стал доступен только для чтения, а сервер ушёл в emergency mode, главное — не спешить. Разберём безопасны ...
Debian/Ubuntu: как исправить Failed to fetch при apt update OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Failed to fetch при apt update

Ошибка Failed to fetch при apt update в Debian и Ubuntu обычно связана не с самим APT, а с DNS, сетью, зеркалом, прокси, временем ...