OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

Staging на VDS с Ansible: единая конфигурация для Nginx, PHP-FPM и Postgres

Разработчикам нужен staging, максимально похожий на прод, а админам — чтобы это не превратилось в зоопарк серверов и конфигов. Разбираем, как построить staging на VDS с помощью Ansible: единые роли для Nginx, PHP-FPM и Postgres, аккуратное inventory, безопасная работа с переменными и секретами.
Staging на VDS с Ansible: единая конфигурация для Nginx, PHP-FPM и Postgres

Staging-окружение на VDS — один из самых недооценённых инструментов для безопасных релизов. Его или вообще нет, или это «какой-то левый сервер», где конфиги руками подкручивали годами, и уже никто не уверен, насколько он похож на прод.

Здоровый подход сегодня — описывать инфраструктуру как код. Для VDS это почти всегда означает Ansible: простой вход, не требуется агент, хорошо подходит для управления Nginx, PHP-FPM и Postgres. Ниже разберём, как навести порядок: сделать повторяемый staging, максимально похожий на прод, но безопасный и управляемый.

Что мы хотим от staging на VDS

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

Типичный чек-лист для staging:

  • Максимально те же версии софта, что и на проде: Nginx, PHP-FPM, Postgres, система (Debian/Ubuntu).
  • Та же топология: хотя бы один frontend (Nginx) и один backend (PHP-FPM + Postgres). В минимальном варианте — всё на одном VDS, но с теми же сервисами.
  • Отдельная база данных: изолированные схемы и пользователи, чтобы никакие тесты не трогали продовые данные.
  • Те же механизмы деплоя: та же сборка артефактов, те же миграции БД, максимально такие же хендлеры перезапуска сервисов.
  • Безопасность: staging не должен «торчать» в интернет как прод, если этого не требуется, а уж точно не должен содержать продовые токены и ключи.

Ansible здесь идеально ложится: мы описываем роли для Nginx, PHP-FPM, Postgres один раз и применяем их и к продовым VDS, и к staging. Разница — только в inventory и переменных окружения.

Базовая структура Ansible-проекта для staging и prod

Самая частая ошибка — смешивать переменные и хосты прод/staging так, что через полгода уже никто не понимает, куда применяется конкретный плейбук. Начнём со структуры директорий, которая явно разводит окружения.

project-ansible/
  ansible.cfg
  inventories/
    prod/
      hosts.ini
      group_vars/
        all.yml
        web.yml
        db.yml
    staging/
      hosts.ini
      group_vars/
        all.yml
        web.yml
        db.yml
  roles/
    common/
    nginx/
    php_fpm/
    postgres/
    app/
  playbooks/
    site.yml
    staging.yml
    prod.yml

Здесь важные моменты:

  • inventories/prod и inventories/staging физически разделены. Это снижает риск случайно применить плейбук по staging-инвентори к продовым серверам.
  • group_vars раздельные: можно спокойно ставить другие лимиты по ресурсам, включать/выключать дебаг, менять домены и порты.
  • Роли общие: roles/nginx, roles/php_fpm, roles/postgres одинаково работают и на staging, и на проде.

Простой пример плейбука для staging:

---
- name: Staging environment
  hosts: all
  become: true

  roles:
    - role: common
    - role: nginx
      tags: [nginx]
    - role: php_fpm
      tags: [php]
    - role: postgres
      tags: [postgres]
    - role: app
      tags: [app]

Запуск для staging:

ansible-playbook -i inventories/staging/hosts.ini playbooks/staging.yml

Структура Ansible-проекта с инвентори, ролями и плейбуками для staging и prod

Inventory: как описать VDS для staging

Inventory можно вести и в YAML, и в INI. Для начала возьмём простой INI-вариант для staging.

[web]
staging-web-1 ansible_host=203.0.113.10 ansible_user=deployer

[db]
staging-db-1 ansible_host=203.0.113.11 ansible_user=deployer

[all:vars]
ansible_python_interpreter=/usr/bin/python3

Если staging у вас компактный и всё крутится на одном VDS, это может выглядеть так:

[web]
staging-all-1 ansible_host=203.0.113.20 ansible_user=deployer roles="web,db"

[db]
staging-all-1

[all:vars]
ansible_python_interpreter=/usr/bin/python3

Дальше уже в ролях можно ориентироваться на группы web/db, а не на конкретные хосты. Это позволит безболезненно разнести компоненты по отдельным VDS в будущем.

Переменные для staging: версии, домены, лимиты

Staging не обязан быть один в один, как прод, по ресурсам, но должен совпадать по версиям и архитектуре приложения. Разделим переменные по группам, чтобы не устраивать свалку из десятков YAML-файлов.

Общие переменные staging

Файл inventories/staging/group_vars/all.yml:

project_name: myapp
project_env: staging

nginx_version: stable
php_fpm_version: "8.2"
postgres_version: "15"

app_domain: staging.example.internal

app_debug: true
app_log_level: debug

# например, другой путь к артефактам
app_release_channel: staging

Здесь фиксируем отличия staging:

  • Домен или поддомен другой.
  • Логи и debug включены сильнее.
  • Можно подключать другие S3-бакеты, очереди и т.п., чтобы не трогать продовые ресурсы.

Переменные для Nginx и PHP-FPM на staging

Файл inventories/staging/group_vars/web.yml:

nginx_worker_processes: auto
nginx_worker_connections: 1024

php_fpm_pm: dynamic
php_fpm_pm_max_children: 10
php_fpm_pm_start_servers: 3
php_fpm_pm_min_spare_servers: 2
php_fpm_pm_max_spare_servers: 5

На staging обычно не нужны такие же высокие лимиты, как на проде. Можно спокойно уменьшить nginx_worker_connections и php_fpm_pm_max_children, чтобы не тратить VDS-ресурсы впустую.

Переменные для Postgres на staging

Файл inventories/staging/group_vars/db.yml:

postgres_listen_addresses: "127.0.0.1"
postgres_port: 5432

postgres_max_connections: 50
postgres_shared_buffers: "512MB"
postgres_work_mem: "8MB"
postgres_maintenance_work_mem: "128MB"

postgres_db_name: myapp_staging
postgres_db_user: myapp_staging
postgres_db_password: "change_me_in_vault"

Пароли и другие чувствительные данные в реальном проекте лучше вынести в Ansible Vault или внешнее хранилище секретов, а не хранить в открытом виде.

Роль для Nginx: общая логика, разные окружения

Создадим роль roles/nginx, которая умеет:

  • устанавливать Nginx нужной версии;
  • конфигурировать общий nginx.conf с параметрами worker-процессов;
  • создавать vhost для приложения с разными доменами и путями для staging/prod.

Пример таска для шаблона конфига сайта:

- name: Deploy nginx site config
  template:
    src: site.conf.j2
    dest: /etc/nginx/sites-available/{{ project_name }}.conf
    owner: root
    group: root
    mode: "0644"
  notify: Reload nginx

- name: Enable nginx site
  file:
    src: /etc/nginx/sites-available/{{ project_name }}.conf
    dest: /etc/nginx/sites-enabled/{{ project_name }}.conf
    state: link
  notify: Reload nginx

Пример шаблона site.conf.j2 (упрощённый, без SSL и лишних настроек):

server {
    listen 80;
    server_name {{ app_domain }};

    root /var/www/{{ project_name }}/current/public;
    index index.php index.html;

    access_log /var/log/nginx/{{ project_env }}-{{ project_name }}.access.log;
    error_log  /var/log/nginx/{{ project_env }}-{{ project_name }}.error.log;

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

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

Ключевой момент: staging и prod используют один и тот же шаблон, но разные переменные app_domain, project_env, php_fpm_version. Это гарантирует одинаковую логику маршрутизации и обращения к PHP-FPM.

Роль PHP-FPM: пулы под окружение

Роль roles/php_fpm настраивает пул для приложения. Хорошая практика — включать в имя пула окружение, чтобы не путаться и не пересекать конфигурации.

Пример таска:

- name: Deploy PHP-FPM pool for app
  template:
    src: app-pool.conf.j2
    dest: /etc/php/{{ php_fpm_version }}/fpm/pool.d/{{ project_env }}-{{ project_name }}.conf
    owner: root
    group: root
    mode: "0644"
  notify: Reload php-fpm

Шаблон app-pool.conf.j2 (фрагмент):

[{{ project_env }}-{{ project_name }}]
user = www-data
group = www-data
listen = /run/php/php{{ php_fpm_version }}-fpm.sock
listen.owner = www-data
listen.group = www-data

pm = {{ php_fpm_pm }}
pm.max_children = {{ php_fpm_pm_max_children }}
pm.start_servers = {{ php_fpm_pm_start_servers }}
pm.min_spare_servers = {{ php_fpm_pm_min_spare_servers }}
pm.max_spare_servers = {{ php_fpm_pm_max_spare_servers }}

php_admin_value[display_errors] = {{ 'On' if project_env == 'staging' else 'Off' }}
php_admin_value[log_errors] = On

В этом примере мы прямо в шаблоне завязываемся на project_env, чтобы на staging включить display_errors. Можно сделать и через переменные, если вы хотите жёстко контролировать такие режимы.

Схематичное взаимодействие Nginx, PHP-FPM и Postgres в staging-окружении на VDS

Роль Postgres: отдельные БД и пользователи для staging

Роль roles/postgres отвечает за базу. Минимальный набор задач:

  • установка нужной версии Postgres;
  • базовый postgresql.conf с тюнингом под VDS;
  • создание пользователя и базы для приложения;
  • настройка pg_hba.conf для безопасного доступа.

Пример создания пользователя и базы:

- name: Ensure PostgreSQL user exists
  become_user: postgres
  postgresql_user:
    name: "{{ postgres_db_user }}"
    password: "{{ postgres_db_password }}"
    role_attr_flags: LOGIN

- name: Ensure PostgreSQL database exists
  become_user: postgres
  postgresql_db:
    name: "{{ postgres_db_name }}"
    owner: "{{ postgres_db_user }}"
    encoding: UTF8
    lc_collate: en_US.UTF-8
    lc_ctype: en_US.UTF-8

Для staging важно убедиться, что:

  • имя базы и пользователя однозначно говорят, что это staging (например, myapp_staging);
  • postgres_listen_addresses ограничены (часто достаточно 127.0.0.1 или приватной сети VDS);
  • данные либо анонимизированы, либо вообще сгенерированы отдельно от продовых.

Роль приложения: код, миграции и конфиг

Роль roles/app отвечает за деплой кода и конфиг приложения. Общая логика:

  • выкачать артефакт релиза (tar.gz или аналог) по тегу или каналу (staging/prod);
  • распаковать в директорию релизов и переключить симлинк current;
  • применить миграции к Postgres;
  • сгенерировать конфиг (например, .env) из шаблона.

Фрагмент таска с конфигом:

- name: Render app env file
  template:
    src: env.j2
    dest: /var/www/{{ project_name }}/current/.env
    owner: www-data
    group: www-data
    mode: "0640"

Пример фрагмента шаблона env.j2 для PHP-приложения:

APP_ENV={{ project_env }}
APP_DEBUG={{ 'true' if app_debug else 'false' }}
APP_LOG_LEVEL={{ app_log_level }}

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT={{ postgres_port }}
DB_DATABASE={{ postgres_db_name }}
DB_USERNAME={{ postgres_db_user }}
DB_PASSWORD={{ postgres_db_password }}

Благодаря этому staging и prod используют одну и ту же роль, а различия минимальны и контролируемы переменными.

Секреты для staging: Ansible Vault и не только

Даже если staging считается менее критичным окружением, хранить там пароли в открытом виде — плохая практика. Лучше сразу приучаться к Ansible Vault или внешним секрет-хранилищам. Если вы хотите пойти ещё дальше и держать секреты в Git-репозитории, посмотрите подход с sops и age в материале о GitOps и управлении секретами.

Пример: вынос секретов staging в отдельный файл inventories/staging/group_vars/all.vault.yml:

postgres_db_password: "s3cr3t_staging"
app_secret_key: "random_app_key"

Шифруем:

ansible-vault encrypt inventories/staging/group_vars/all.vault.yml

И подключаем его, например, через include_vars в плейбуке или оставляя Ansible возможность автоматически подхватить файл с расширением .vault.yml, если так принято в вашем проекте.

Запуск плейбука со вводом Vault-пароля:

ansible-playbook -i inventories/staging/hosts.ini playbooks/staging.yml --ask-vault-pass

Как не перепутать staging и prod при запуске Ansible

Человеческий фактор никуда не делся: можно запустить не тот плейбук по не тому inventory. Есть несколько практик, которые помогают уменьшить вероятность ошибок.

  • Разные ansible.cfg для prod и staging (или хотя бы разные алиасы команд). Например, make deploy-staging и make deploy-prod с жёстко прописанными путями к inventory.
  • Явное указание окружения в плейбуке и проверка переменной project_env.
  • Промпт-предупреждение для продового плейбука через vars_prompt.

Пример простой проверки окружения в плейбуке:

- name: Ensure environment is staging
  hosts: all
  gather_facts: false
  tasks:
    - name: Fail if project_env is not staging
      fail:
        msg: "This playbook should be run only for staging environment"
      when: project_env != 'staging'

Аналогично можно сделать жёсткий safeguard в продовом плейбуке, чтобы он не запускался по staging-инвентори.

Staging-данные в Postgres: как их готовить

Отдельная тема — содержимое базы на staging. Вариантов несколько:

  • Синтетические данные, генерируемые миграциями или сидерами (например, команды фреймворка Laravel или Symfony).
  • Регулярный дамп продовой базы с анонимизацией чувствительных полей.
  • Комбинация: структура и часть данных с прода, плюс генерация недостающих.

Автоматизировать этот процесс через Ansible тоже возможно. Например:

- name: Run database seed on staging
  become: true
  become_user: www-data
  args:
    chdir: /var/www/{{ project_name }}/current
  command: php artisan db:seed --env=staging
  when: project_env == 'staging'

Главное — не подключать staging напрямую к продовой БД и не использовать продовые креденшелы. Это частая и очень опасная ошибка, особенно если staging доступен из интернета по доменному имени; в подобных сценариях не забывайте про корректные SSL-сертификаты и жёсткие ACL.

Типичный полный цикл: поднять новый staging VDS с нуля

Соберём всё вместе в сценарий, который пригоден для практики.

  1. Создаёте один или два VDS под staging (web и db, либо всё на одном) с нужной ОС.
  2. Прописываете их в inventories/staging/hosts.ini, как показано выше.
  3. Заполняете group_vars для staging: версии Nginx, PHP и Postgres, домены, ресурсы.
  4. Создаёте или дорабатываете роли nginx, php_fpm, postgres, app так, чтобы они не содержали «зашитых» окружений.
  5. Выносите секреты staging в Ansible Vault.
  6. Запускаете плейбук staging и проверяете, что:
  • Nginx отвечает по staging-домену,
  • PHP-FPM запустился с нужным пулом и лимитами,
  • Postgres слушает нужный порт и база myapp_staging создана,
  • приложение может подключиться к БД и пройти миграции.

После этого staging можно включать в CI/CD: перед выкатыванием в прод гоняем прогон тестов, проверяем миграции и работоспособность приложения на staging. А если вам нужно без простоя перенести боевой сайт, пригодится отдельный разбор про миграцию без даунтайма — см. материал о переносе сайтов без простоя.

Типичные грабли и как их избегать

Из практики админов и девопсов по staging на VDS с Ansible чаще всего встречаются такие проблемы:

  • Роли жёстко хардкодят окружение — например, в шаблоне Nginx встречается server_name example.com без переменной. Решение: вытаскивать всё, что отличается между окружениями, в group_vars.
  • Одинаковые имена баз и пользователей для staging и prod. В итоге скрипт или человек легко ошибается. Решение: всегда использовать суффикс или префикс окружения (myapp_staging, myapp_prod).
  • Случайное применение плейбука staging к продовым хостам. Решение: раздельные inventory, защитные проверки project_env, явные make-таргеты.
  • Staging зависим от продовых внешних сервисов (S3, очереди, платежи) в боевом режиме. Решение: отдельные креденшелы и sandbox-аккаунты, а в крайнем случае — мок-сервисы.
  • Разъехавшиеся версии Postgres и PHP между staging и продом. Решение: прописывать версии как переменные в group_vars и регулярно сверять.

Итоги

Staging на VDS, описанный через Ansible, — это не роскошь, а базовый инструмент для безопасных релизов. Как только вы раскладываете конфигурацию по ролям, разделяете inventory и переменные для staging и prod, жизнь сильно упрощается:

  • Новые VDS под staging поднимаются и настраиваются в один плейбук.
  • Разработчики получают предсказуемое окружение, близкое к продовому.
  • Админы могут быстро править и донастраивать Nginx, PHP-FPM и Postgres в одном месте.
  • Релизы через staging становятся нормой: фичи и миграции прогоняются на боевом стеке до выката в прод.

Главное — держать staging и prod на одной технологической базе, но с аккуратно разведёнными переменными, секретами и доступами. Тогда Ansible и VDS полноценно отработают свою роль как основа для управляемых и предсказуемых окружений.

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

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

Нагрузочное тестирование staging и prod: практический гид для админов OpenAI Статья написана AI (GPT 5)

Нагрузочное тестирование staging и prod: практический гид для админов

Разберем, как системно подойти к нагрузочному тестированию веб‑проектов: чем реально отличается staging от prod, как строить профи ...
VDS: шифрование диска с LUKS2 и autounlock без ручного ввода пароля OpenAI Статья написана AI (GPT 5)

VDS: шифрование диска с LUKS2 и autounlock без ручного ввода пароля

Разберём, как включить шифрование диска с LUKS2 на VDS и не вводить пароль после каждой перезагрузки. Пошагово создадим LUKS2-том, ...
cron vs systemd timers: что выбрать для задач и healthcheck OpenAI Статья написана AI (GPT 5)

cron vs systemd timers: что выбрать для задач и healthcheck

Cron до сих пор жив на большинстве серверов, но в современных Linux-дистрибутивах с systemd таймеры дают более управляемый и наблю ...