Locale
Быстрое развертывание стэка ПО на web-серверах для запуска Laravel приложений
Артур Хайбуллин
Артур Хайбуллин 13 апр 2026, 00:18
Снимок экрана 2026-04-16 103916.png

Когда у тебя один проект и один сервер — всё просто: авторизовался на сервере по SSH, поставил nginx, PHP, настроил конфиги, готово. Но как только проектов становится больше — это превращается в рутину, которая пожирает время и плодит ошибки.

Я ловил себя на том, что каждый раз делаю одно и то же:

  • Устанавливаю nginx из официального репозитория (а не из дистрибутива, чтобы иметь последнюю версию)

  • Создаю PHP-FPM пул для каждого сайта с отдельным Unix-сокетом

  • Настраиваю OPcache, лимиты памяти, время выполнения...

  • Настраиваю виртуальные хосты nginx с правильными заголовками безопасности

  • Устанавливаю Composer, Node.js, Yarn

  • Создаю структуру директорий для Laravel

  • Настраиваю cron для artisan schedule:run

И каждый раз — вручную. Каждый раз — с риском что-то забыть или сделать немного не так, как на соседнем сервере.

Почему Ansible, а не Docker?

ansible-1024x683-1-4034155887.png

Хороший вопрос. Docker отлично подходит для изоляции и переносимости приложений, но у многих небольших проектов и заказчиков есть требования или ограничения, при которых контейнеры избыточны или неудобны: VDS с ограниченными ресурсами, хостинг без поддержки Docker, проекты где нужен прямой доступ к "голому" серверу. Кроме того, поддерживать legacy-серверы без Docker — реальность для многих разработчиков.

Ansible же работает с любым Linux-сервером через SSH. Никакого агента, никакой инфраструктуры — только Python на целевом хосте.

Что в итоге получилось

Роль nginx_laravel разворачивает полноценную Laravel-среду одной командой:

ansible-playbook site.yml

Что происходит под капотом:

  • nginx устанавливается из официального репозитория nginx.org (не из apt/dnf дистрибутива). Версия автоматически определяется со страницы загрузок — берётся последняя стабильная ветка (чётный минорный номер: 1.26.x, 1.28.x и т.д.).

  • PHP 8.4 (или любая версия >= 8.2) ставится из репозитория ondrej/php на Debian/Ubuntu или Remi на CentOS/AlmaLinux. Для каждого сайта создаётся отдельный PHP-FPM пул с изолированным Unix-сокетом.

  • Несколько сайтов на одном сервере — в конфиге просто список, остальное делает роль.

  • Сайт по умолчанию (catch-all) всегда присутствует: показывает информацию о сервере, версии ПО, IP клиента. Полезно для проверки того, что сервер вообще жив.

  • Безопасность из коробки: заголовки X-Frame-Options, X-Content-Type-Options, Referrer-Policy, server_tokens off, закрытый доступ к .env, .git, vendor.

  • GitLab CI/CD: в репозитории уже есть пайплайн — yaml-lint, ansible-lint, syntax-check, деплой и HTTP smoke-тест каждого сайта после деплоя.

Один из принципов, который я хотел заложить с самого начала: любое изменение в репозитории должно быть проверено автоматически, а деплой на сервер — только если все проверки прошли.

Снимок экрана 2026-04-16 104015.png

Пайплайн состоит из трёх стадий: validate → deploy → verify.

validate ──────────────────────────────────────────────────────┐
  yaml-lint   ──┐                                              │
  lint          ├── все три параллельно                        │
  syntax-check ─┘                                              │
                                                               ▼
deploy ────────────────────────────────────────────────────────┐
  deploy:production  (только если validate прошёл)             │
                                                               ▼
verify ────────────────────────────────────────────────────────┘
  verify:sites  (только если deploy прошёл)

Стадия Validate (Предпроверка)

Запускается на каждый пуш в main и на каждый merge request. Три задачи выполняются параллельно.

yaml-lint — проверяет синтаксис всех .yml и .yaml файлов в репозитории. Это первая линия обороны: опечатка в отступе или забытое двоеточие не уйдёт дальше этой проверки.

yamllint --config-file .yamllint.yml --format colored $(find . -name '*.yml' -o -name '*.yaml')

ansible-lint — статический анализатор специально для Ansible. Он знает об идиомах и антипаттернах: использование shell там, где можно обойтись command, опасные pipe-команды без pipefail, шаблоны Jinja2 не в конце имени задачи, захардкоженные пароли и прочее. Профиль production — самый строгий, он требует соблюдения всех правил.

Несколько примеров того, что он поймал у меня в процессе разработки роли:

  • Имя задачи Install PHP {{ php_version }} extensions — шаблон посередине строки, нарушает правило name[template]

  • curl ... | bash - без set -o pipefail — правило risky-shell-pipe

  • Хардкод www-data в задачах вместо переменной — логическая ошибка, которую lint не поймает, но code review — да

syntax-check — прогоняет Ansible через разбор плейбука без реального подключения к серверу:

ansible-playbook site.yml --syntax-check --inventory inventory/hosts.yml

Здесь выявляются ошибки в структуре плейбука, несуществующие переменные в определённых контекстах, некорректные модули.

Стадия Deploy (Публикация)

Запускается только при пуше в main — не на MR. Это принципиально: merge request'ы проходят только валидацию, реальный деплой происходит только когда код принят в основную ветку.

resource_group: production

resource_group гарантирует, что одновременно может быть только один активный деплой на production. Если два пуша попали в main почти одновременно — второй деплой дождётся завершения первого.

Что делает джоб:

1. SSH-ключ. GitLab хранит приватный ключ в переменной SSH_PRIVATE_KEY. Джоб запускает ssh-agent, загружает ключ и добавляет хосты в known_hosts через ssh-keyscan. Ключ нигде не пишется на диск в открытом виде — только в памяти агента.

2. ansible-vault (опционально). Если в репозитории есть зашифрованные секреты, переменная ANSIBLE_VAULT_PASSWORD позволяет их расшифровать. Пароль пишется во временный файл, удаляется в after_script — даже если плейбук упадёт.

3. Плейбук. Запускается с флагом --diff — в логах CI видно, что именно изменилось на сервере: какие строки добавились в конфиги, какие файлы созданы. Это важно при разборе инцидентов.

ansible-playbook site.yml --inventory inventory/hosts.yml --diff

Стадия Verify (Пост-проверка)

Самая интересная стадия. Запускается после успешного деплоя и проверяет, что сайты реально отвечают.

Написана на чистом Python без внешних зависимостей (только pyyaml). Скрипт читает inventory/hosts.yml, достаёт список хостов и сайтов — и для каждого делает HTTP-запрос.

Два типа проверок:

Сайт по умолчанию — запрос к IP-адресу сервера без заголовка Host. Должен вернуть 2xx/3xx. Это проверяет, что nginx запущен и дефолтный catch-all сайт живой (у нас он отдаёт PHP-страницу с инфо о сервере).

Именованные сайты — запрос к IP-адресу сервера, но с заголовком Host: домен. Это имитирует реальный запрос от браузера через DNS, без необходимости иметь настроенный DNS в CI-окружении. Для HTTPS-сайтов — запрос через https:// с отключённой проверкой сертификата (поддержка self-signed).

def probe(url, host_header=None, ssl_ctx=None):
    headers = {"Host": host_header} if host_header else {}
    req = urllib.request.Request(url, headers=headers)
    ...

Если хотя бы один сайт вернул 4xx/5xx или не ответил — джоб падает, пайплайн красный. В логах видно конкретно какой сайт и какой код вернул.

Почему это важно

До пайплайна типичная история выглядела так: изменил шаблон nginx, запушил, через 10 минут проверяешь и оказывается что сайт лежит. Это тормозит работу. С пайплайном — если изменение сломало nginx-конфиг, syntax-check поймает это ещё до деплоя. Если деплой прошёл, но что-то не так запустилось — verify:sites покажет это сразу, пока разработчик ещё смотрит на пайплайн.

Весь цикл от пуша до подтверждения что сайты живы — около 3–4 минут.

Что мне кажется важным в дизайне

Конфигурация как данные, а не код. Вся конфигурация сайтов — это просто YAML в инвентаре:

laravel_sites:
  - domain: "myapp.ru www.myapp.ru"
    root: "/var/www/myapp"
    ssl_enabled: true
    ssl_email: "admin@myapp.ru"
    env: production
Снимок экрана 2026-04-13 001232.png

Добавляешь сайт — добавляешь элемент в список. Никаких дополнительных файлов, никакой магии.

Идемпотентность. Можно запускать роль сколько угодно раз — на уже настроенном сервере она только проверит, что всё в порядке, и ничего не тронет. Это важно для CI/CD: деплой выглядит одинаково что в первый раз, что в сотый.

Несколько доменов для одного сайта. domain: "site.ru www.site.ru" — первый домен используется для сокета, логов, сертификата; nginx обслуживает все.

Кроссплатформенность. Изначально роль была только для Debian/Ubuntu. Потом появились серверы на AlmaLinux — и пришлось добавить поддержку RedHat-семейства. Рефакторинг свёлся к тому, чтобы вынести OS-специфичные переменные (пути к сокетам, имена сервисов, пакеты) в отдельные файлы vars/Debian.yml и vars/RedHat.yml. Остальной код остался тем же.

Что ещё не сделано

  • HTTPS через Let's Encrypt (certbot) — в конфиге уже есть переменные, шаблон nginx готов, но автоматическое получение сертификата через certbot пока нужно делать вручную

  • Поддержка PostgreSQL/MySQL как опции при настройке сервера

  • Мониторинг через php-fpm status page

  • автоматическая публикация кода сайта из репозитория, при указании ссылки на репозиторий. Возможно реализую через webhook после успешного развертывания сервера и сайта.

Такая роль — это не серебряная пуля и не замена пониманию того, как работает nginx и PHP-FPM. Это инструмент, который убирает рутину и делает настройку воспроизводимой. Если у тебя несколько Laravel-проектов на VDS и ты каждый раз настраиваешь сервер вручную — попробуй. В худшем случае прочитаешь код и найдёшь что-то полезное для себя.

Исходники: GitOps / ansible-web-server · GitLab

Комментарии (0)
Комментировать
Пока нет комментариев

Станьте первым, кто прокомментирует эту запись