На практике у многих проектов один-единственный 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-devmyapp-stagemyapp-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.

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% он не получит, и прод не просядет.

Пример с 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, а потом позволяет безболезненно разнести среды по разным серверам, когда проект вырастет.


