ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

Nginx/PHP-FPM: Too many levels of symbolic links (ELOOP) — как найти и исправить symlink loop при деплое

ELOOP («Too many levels of symbolic links») часто всплывает после деплоя через симлинк current в связке Nginx и PHP-FPM. Разберём симптомы, команды readlink -f/find -L/namei, влияние realpath_cache и безопасный паттерн релизов без даунтайма.
Nginx/PHP-FPM: Too many levels of symbolic links (ELOOP) — как найти и исправить symlink loop при деплое

Ошибка Too many levels of symbolic links почти всегда означает одно: в файловой системе получилась петля символических ссылок, и ядро возвращает ELOOP при попытке «развернуть» путь. В веб‑стеке это обычно проявляется как open() failed в Nginx и как ошибки include/require/autoload в PHP‑приложении через PHP‑FPM (иногда не сразу — из‑за кэша realpath).

Ниже — практичная инструкция для продакшена: как быстро доказать, что это именно symlink loop, найти место зацикливания и исправить так, чтобы проблема не возвращалась при следующем деплое с переключением current.

Как выглядит ELOOP: симптомы в логах Nginx и PHP

Типовые признаки, по которым проще всего начать расследование:

  • В /var/log/nginx/error.log: open() "/path/to/file" failed (40: Too many levels of symbolic links).
  • В PHP‑логах/STDERR пула: ошибки открытия файлов, падение автозагрузчика, проблемы с шаблонами и конфигами.
  • Время появления совпало с релизом, где current переключали на новый каталог (releases/shared схема).

Важно: «40» в сообщении Nginx на Linux — это тот же ELOOP. И Nginx, и PHP видят только факт: конечный файл недостижим, потому что цепочка симлинков зациклилась или стала «слишком длинной».

Откуда берётся петля: частые причины при деплое через current/releases

В реальных инцидентах чаще всего встречаются такие сценарии:

  1. Петля current ↔ releases: например, релизный каталог (или его часть) ссылается обратно на current или на родителя, а current при этом указывает на релиз.
  2. Относительные пути в ln -s из «не того» каталога: скрипт создаёт ссылку, но target получается не тем, что вы ожидали.
  3. Конфликт shared ↔ release: сделали current/storageshared/storage, а позже (или другим скриптом) появилось shared/storagecurrent/storage.
  4. Перенос релизов rsync/архивацией: симлинки «переехали» иначе, чем вы думали (особенно при разных ключах сохранения/разыменования ссылок).
  5. Инструменты приложения: CMS‑плагины, генераторы статики, сборщики ассетов, которые создают симлинки во время работы и «перекрещивают» дерево.

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

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Перед тем как править, зафиксируйте текущую картину: куда указывает current, что лежит в shared, и какие симлинки появились «внутри» релиза. Это ускорит разбор, если ошибка повторится.

Структура деплоя с каталогами current, releases и shared в терминале

Диагностика: как быстро доказать symlink loop и найти точку зацикливания

Начинайте от конкретного пути, который фигурирует в ошибке. Не «сканируйте весь сервер», пока не проверили один проблемный файл/каталог из error.log.

Шаг 1. Посмотреть, куда реально указывают ссылки: ls -la

Проверяем ключевые узлы схемы деплоя:

ls -la /var/www/site
ls -la /var/www/site/current
ls -la /var/www/site/releases
ls -la /var/www/site/shared

Ищите «подозрительные» направления: ссылки из релиза или shared назад на current или на верхние директории проекта.

Шаг 2. Получить канонический путь: readlink -f

readlink -f разворачивает путь до конца. При петле часто вернёт ошибку или не сможет получить результат.

readlink -f /var/www/site/current
readlink -f /var/www/site/current/public/index.php
readlink -f /var/www/site/current/vendor/autoload.php

Если команда «спотыкается», у вас почти наверняка именно ELOOP из-за симлинков (а не, скажем, права или отсутствующий файл).

Шаг 3. Найти симлинки, которые «утягивают» дерево: find -L

Ключ: -L заставляет find следовать по симлинкам. На зацикливании он начинает натыкаться на проблему и это видно по поведению и сообщениям.

find -L /var/www/site/current -maxdepth 5 -type l -ls
find -L /var/www/site -maxdepth 7 -type l -ls

Смотрите, какие ссылки ведут «не туда»: назад в current, в родителя или в неожиданные места вне структуры releases/shared.

Шаг 4. Когда readlink не помогает: namei -l по компонентам

Если цепочка сложная и нужно увидеть «на каком шаге» путь начинает ездить по кругу:

namei -l /var/www/site/current/public/index.php

Это удобно ещё и тем, что сразу видны права на каждом компоненте (бывает полезно, когда параллельно есть проблема с доступом).

PHP-FPM и realpath_cache: почему симптомы могут «плавать»

После переключения current PHP может вести себя непредсказуемо, если воркеры держат старые развернутые пути в realpath cache. В итоге возможна ситуация: симлинки уже починили, Nginx уже «видит» корректный путь, а часть PHP‑воркеров продолжает пытаться включать файлы по старым, уже невалидным цепочкам.

Быстро посмотреть настройки кэша:

php -i | grep -E 'realpath_cache_size|realpath_cache_ttl'

Для расследования иногда полезно временно уменьшить realpath_cache_ttl, но главный практический шаг после исправления симлинков — корректно перезагрузить PHP‑FPM (reload или restart), чтобы воркеры сбросили состояние.

Где смотреть ошибки PHP-FPM, если всё под systemd

Не ограничивайтесь только файловыми логами пула: journald часто быстрее показывает картину по времени деплоя.

systemctl status php-fpm
journalctl -u php-fpm --since "1 hour ago"
journalctl -u php8.3-fpm --since "1 hour ago"

Имя юнита зависит от дистрибутива/версии. Ищите по ключевым словам ELOOP, Too many levels, realpath.

Если вы разворачиваете проекты на общем сервере и хотите уменьшить риск таких инцидентов за счёт предсказуемого окружения, посмотрите тарифы на виртуальный хостинг (для типовых PHP‑сайтов) или переходите на VDS (если нужна полная свобода с системными настройками и деплоем).

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Как исправить петлю без даунтайма: два рабочих сценария

Точная тактика зависит от того, где именно образовалась петля. Важно: не «чистите всё подряд» — ваша задача вернуть current в корректное состояние и убрать встречные ссылки, создающие круг.

Сценарий A: зациклился current

Если проблема в current (например, он указывает не туда или на относительный target, который сам приводит обратно), самый безопасный путь — пересоздать current как ссылку на правильный релиз.

Проверка перед правкой:

cd /var/www/site
ls -la current
ls -la releases

Пересоздание симлинка:

cd /var/www/site
rm -f current
ln -s releases/2026-01-22_120000 current
ls -la current
readlink -f /var/www/site/current

Дальше — применяем reload:

systemctl reload php-fpm
systemctl reload nginx

Если видите, что часть воркеров продолжает «жить прошлым» (типично при realpath‑кэше и долгоживущих процессах), лучше сделать restart PHP‑FPM в окно минимальной нагрузки:

systemctl restart php-fpm

Сценарий B: петля между shared и релизом

Это самый коварный вариант в схеме releases/shared/current. Базовый принцип простой:

  • shared не должен ссылаться на current или конкретный релиз.
  • Ссылки должны идти «из релиза в shared», а не наоборот.

Проверяем, нет ли ссылок из shared на current:

find /var/www/site/shared -maxdepth 5 -type l -ls

Если видите что-то вроде shared/storage -> /var/www/site/current/storage, это почти гарантированная причина петли (особенно если в релизе сделана обратная ссылка current/storageshared/storage). Исправление: удаляем или пересоздаём проблемный симлинк в shared так, чтобы он указывал на реальный каталог внутри shared, а не на current.

Когда виноват не деплой: связка root/alias/try_files в Nginx

Бывает, что петля действительно есть, но вы ищете её «не там», потому что путь сформирован логикой Nginx. Два частых источника путаницы:

  • root в одном контексте и неожиданный try_files, который проверяет альтернативные кандидаты (петля может быть во втором или третьем варианте).
  • alias в location (он подставляет путь иначе, чем root), и ошибка проявляется только на части URI.

Снимите эффективный конфиг и восстановите соответствие URI → filesystem:

nginx -T | sed -n '1,220p'

Дальше находите нужный server_name, смотрите root или alias, затем try_files. Если вас интересует ещё и «ветвление» логики в Nginx, может пригодиться материал про nginx map для кеширования форматов WebP/AVIF: там наглядно видно, как альтернативные пути в конфиге приводят к неожиданным обращениям к файловой системе.

Как предотвратить ELOOP в будущем: безопасный паттерн deploy symlink

В продакшене лучше один раз «зашить» защиту в процесс деплоя, чем потом ловить ELOOP ночью.

1) Делайте ссылки абсолютными или жёстко контролируйте рабочую директорию

Большинство случайных петель появляются из-за относительных путей, созданных из неверного каталога. Если используете относительные ссылки — фиксируйте cd перед ln -s и проверяйте результат readlink.

2) Добавьте preflight-проверку в CI/CD перед переключением current

Ещё до перевода трафика на новый релиз проверяйте, что ключевые файлы корректно развернутся:

readlink -f /var/www/site/releases/2026-01-22_120000/public/index.php
readlink -f /var/www/site/releases/2026-01-22_120000/vendor/autoload.php

И что в shared нет ссылок на current:

find /var/www/site/shared -maxdepth 5 -type l -print

3) После переключения current перезагружайте сервисы осознанно

Для Nginx обычно достаточно reload. Для PHP‑FPM reload часто нормальный вариант, но при странностях из‑за realpath и долгоживущих воркеров быстрее и честнее сделать restart в контролируемое окно.

4) Держите под рукой «ночной» набор команд

tail -n 200 /var/log/nginx/error.log
journalctl -u nginx --since "30 min ago"
journalctl -u php-fpm --since "30 min ago"

Если вы дополнительно используете кеши (Redis/Memcached) и логика приложения «разветвляется», полезно иметь понятную картину, где именно приложение берёт пути и артефакты. В этом контексте может быть полезна заметка про кеширование в PHP через Redis и Memcached — она помогает быстрее исключать «не файловые» причины похожих симптомов.

Анализ цепочки пути и симлинков командами namei и readlink

Шпаргалка: что делать, когда «горит»

  1. Возьмите точный путь из open() failed и проверьте его readlink -f.
  2. Проверьте current и его target: ls -la.
  3. Просканируйте симлинки по дереву: find -L с ограничением глубины.
  4. Если петля shared ↔ release — уберите ссылки из shared в current (shared должен быть «реальным»).
  5. Сделайте reload или restart PHP‑FPM (учитывая realpath_cache_ttl) и reload Nginx.
  6. Добавьте preflight‑проверки в деплой, чтобы ELOOP больше не повторялся.

Если у вас атомарные релизы и несколько сервисов (Nginx, PHP‑FPM, воркеры), воспринимайте симлинки как контракт: один неверный указатель превращается в ELOOP, который выглядит как «упал веб». Но с диагностикой через readlink -f, find -L, namei и journald проблема обычно находится за 5–15 минут и потом устраняется на уровне процесса деплоя.

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

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

Nginx 499 Client Closed Request: причины, тайминги и как снизить процент обрывов OpenAI Статья написана AI (GPT 5)

Nginx 499 Client Closed Request: причины, тайминги и как снизить процент обрывов

Код 499 в Nginx означает, что клиент закрыл соединение до ответа сервера. Это может быть браузер, мобильная сеть, gRPC‑клиент или ...
systemd-resolved и Docker: что происходит с /etc/resolv.conf и почему не работает DNS в контейнерах OpenAI Статья написана AI (GPT 5)

systemd-resolved и Docker: что происходит с /etc/resolv.conf и почему не работает DNS в контейнерах

Типичная поломка на Ubuntu/Debian: на хосте DNS работает, а в Docker-контейнерах появляется Temporary failure in name resolution. ...
Nginx: large_client_header_buffers и ошибка 400 Request Header Or Cookie Too Large OpenAI Статья написана AI (GPT 5)

Nginx: large_client_header_buffers и ошибка 400 Request Header Or Cookie Too Large

Ошибка 400 Request Header Or Cookie Too Large в Nginx обычно связана с раздутыми cookie или длинными заголовками из цепочки прокси ...