MAATRIX / Блог / Три версии одного приложения на машине: какую из них трогать нельзя

Три версии одного приложения на машине: какую из них трогать нельзя

MAATRIX

Заходите в /var/www или /opt на унаследованном сервере — и вместо одного приложения видите три: app, app_new, app2024. Или три отдельных systemd-юнита с почти одинаковыми именами, каждый слушает свой порт. Все три теоретически рабочие: запускаются, отвечают на запросы, ничего не падает при systemctl status. Вопрос «какую из них можно удалить» звучит просто, а ответ на глаз не виден — угадаете неправильно, и через десять минут в саппорт посыплются жалобы, что сайт не открывается. Разберём, как по трафику, логам и конфигу веб-сервера точно определить боевую версию, прежде чем трогать хоть один каталог.

Почему на одном сервере оказывается несколько версий одного приложения

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

  • Тестовая копия для проверки обновления. Кто-то скопировал прод в app_test или app_v2, чтобы проверить новую версию фреймворка или миграцию базы, не трогая боевой каталог. Проверка прошла успешно (или неуспешно), копию собирались удалить — и забыли, потому что задача формально была закрыта.
  • Незавершённый переезд на новую версию. Команда решила перейти с Python 2 на Python 3, или с Laravel 8 на Laravel 10, подняла параллельную инсталляцию на соседнем порту, планировала переключить трафик «на следующей неделе» — и переключение отложилось на месяцы, а состав команды сменился.
  • A/B-эксперимент или canary-релиз, который не убрали после эксперимента. Технически это осознанное дублирование, но реестра «зачем это здесь» часто не остаётся — эксперимент закончился, вывод сделали, а инфраструктуру для него не разобрали.
  • Подрядчик разворачивал новую версию рядом со старой как страховку на случай отката, сдал проект и ушёл — а откатывать в итоге не пришлось, и лишняя копия осталась лежать мёртвым грузом.
  • Ручной деплой поверх автоматического. Кто-то в спешке развернул хотфикс вручную в отдельный каталог, вместо того чтобы разобраться, как работает штатный CI/CD — и вместо одного пайплайна получилось два параллельных источника кода.

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

Первый шаг: найти все копии, а не только очевидные

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

Начните с поиска одинаковых по смыслу каталогов рядом друг с другом:

ls -la /var/www/ /opt/ /srv/ /home/*/apps/ 2>/dev/null
find / -maxdepth 4 -iname "*app*" -type d 2>/dev/null | grep -viE "node_modules|\.git|vendor"

Обращайте внимание не только на явные суффиксы вроде _old, _new, _backup, _v2, _test, но и на менее очевидные признаки: каталоги с разными датами последнего изменения при одинаковой структуре файлов, или каталоги, отличающиеся только регистром и годом в названии (shop, shop2023, Shop_final).

Дальше — процессы и systemd-юниты, которые могут запускать разные копии на разных портах:

ps auxf | grep -iE "node|gunicorn|php-fpm|java|python" 
systemctl list-units --type=service --all | grep -iE "app|api|shop|backend"
ss -tulpn | grep -E ":(3000|3001|4000|4001|8000|8080|8081)"

Если приложение живёт в Docker, список копий может прятаться в контейнерах, а не в каталогах на хосте: docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}" и docker images | grep -i app.

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

ls -la /proc/<PID>/cwd
cat /proc/<PID>/cmdline | tr '\0' ' '

А если процесс вообще не виден ни в systemctl, ни в crontab, а запущен когда-то вручную через nohup или в забытой screen-сессии — это отдельный, более общий случай, разобранный в статье «Процесс работает мимо systemd: кто его запустил и что будет после ребута». Здесь достаточно зафиксировать сам факт и порт, чем он управляется, и идти дальше.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Порт в конфиге веб-сервера — самый быстрый и надёжный признак

Если перед приложением стоит nginx или Apache (а в подавляющем большинстве случаев так и есть), у вас уже есть готовый и достоверный источник истины: конфиг веб-сервера явно указывает, на какой порт или сокет идёт трафик снаружи.

Для nginx:

grep -rn "proxy_pass\|server_name" /etc/nginx/sites-enabled/ /etc/nginx/conf.d/

Найдите директиву proxy_pass http://127.0.0.1:PORT для нужного домена и сопоставьте порт с портами из вашего списка копий (ss -tulpn с предыдущего шага). Копия, на порт которой указывает proxy_pass активного (не закомментированного, не лежащего в sites-available без симлинка в sites-enabled) конфига — и есть боевая.

Для Apache — аналогично, но смотреть нужно ProxyPass в активных VirtualHost:

apache2ctl -S
grep -rn "ProxyPass\|DocumentRoot" /etc/apache2/sites-enabled/

Здесь важны две ловушки, которые сводят на нет весь смысл проверки:

  • Конфиг может быть неактуальным. Файл в sites-enabled не гарантирует, что nginx его вообще подхватил — проверьте nginx -T (полный дамп активной конфигурации после разбора всех include) и сверьте с тем, что реально видно на диске. Расхождение само по себе диагностика: значит, где-то есть ещё один include или override, который вы ещё не нашли.
  • DocumentRoot/root для статики и proxy_pass/ProxyPass для бэкенда — разные вещи. Если root смотрит в /var/www/app, а proxy_pass — на порт процесса из /var/www/app_v2, у вас смешанная конфигурация, и это отдельная проблема, которую стоит зафиксировать явно, а не молча починить в одну сторону.

Если конфиг веб-сервера однозначно указывает на одну копию — этого обычно достаточно, чтобы с высокой уверенностью назвать её боевой. Но «высокой уверенностью» не значит «стопроцентной»: конфиг мог устареть точно так же, как и код. Следующие два шага — трафик и логи — нужны, чтобы подтвердить вывод фактическими данными, а не одним текстовым файлом.

Трафик и логи обращений: кто реально отвечает пользователям

Конфиг веб-сервера говорит, куда должен идти трафик. Логи говорят, куда он идёт на самом деле — и это не всегда одно и то же, особенно если в истории сервера были неаккуратные правки или балансировка между несколькими бэкендами.

Смотрите access-лог nginx с временными метками — если в нём есть upstream_addr (адрес бэкенда, который обработал запрос), это прямое доказательство. Если формат лога его не включает, добавьте временно, на период диагностики:

log_format upstream_debug '$remote_addr - $time_local "$request" $status upstream=$upstream_addr';
access_log /var/log/nginx/access_debug.log upstream_debug;

После nginx -s reload в новом логе будет явно виден порт бэкенда, обработавшего каждый запрос — сравните с портами кандидатов из первого шага.

Второй источник — сетевая активность самого процесса. Если nginx точно проксирует на конкретный порт, но вы хотите убедиться, что запросы реально долетают до процесса за ним, а не обрываются где-то по пути:

tcpdump -i lo -n port 3001 -c 20

Двадцать пакетов на активном сайте появятся за секунды; тишина в течение минуты на порту, который теоретически «боевой» по конфигу — сигнал, что либо трафика сейчас объективно мало, либо конфиг всё же врёт, и стоит вернуться к предыдущему шагу.

Третий и самый простой признак, который не требует включать debug-логи — возраст файлов самого приложения и логов внутри его каталога:

find /var/www/app_candidate -name "*.log" -newer /tmp/marker -mmin -60
stat /var/www/app_candidate/storage/logs/*.log 2>/dev/null | grep Modify
ls -la --time-style=full-iso /var/www/app_candidate/storage/logs/ 2>/dev/null | tail -5

Если приложение пишет собственные логи (Laravel, Django, большинство Node-фреймворков), свежесть этих файлов — почти всегда точный индикатор: заброшенная копия молчит неделями, боевая пишет каждую минуту. Это дополняет проверку конфига независимым источником: если и proxy_pass, и свежие логи приложения указывают на один каталог, вероятность ошибки крайне мала.

Заброшенные копии: как отличить staging от «забыли выключить»

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

Проверьте у каждой некандидатской копии несколько признаков, прежде чем решать её судьбу:

  • Есть ли у неё собственный домен или поддомен. staging.example.com, указывающий на этот порт — весомый аргумент, что копия используется осознанно, а не заброшена. Проверьте DNS-записи и конфиг веб-сервера на предмет отдельного server_name.
  • Обращается ли к ней кто-то снаружи прямо по IP:порту. Если домена нет, но порт открыт наружу, кто-то может ходить на него напрямую — по закладке в браузере, по интеграции, из мобильного приложения со старым хардкодом адреса. tcpdump -i any -n port <port> -c 50 за несколько дней покажет, есть ли внешние обращения.
  • Совпадает ли база данных. Если тестовая копия подключена к той же боевой базе (проверьте .env/config), удаление кода безопасно; если пишет в собственную тестовую базу — заодно решите судьбу и её.
  • Кто последний правил код внутри. git log -1 в каталоге или дата последнего изменения файлов покажет, была ли это разовая копия для одной проверки два года назад или живой, регулярно обновляемый контур.

Если ни у одной из проверок нет однозначного ответа «используется», переведите процесс в состояние наблюдения, а не убирайте сразу:

# остановить сервис, но не удалять файлы и не снимать из systemd навсегда
systemctl stop app_old.service
# зафиксировать текущее состояние в комментарии к юниту или в отдельном файле-заметке
echo "Остановлено $(date), причина: похоже на заброшенную тестовую копию. Удалить после 2026-10-01, если жалоб не будет." > /opt/app_old/DECOMMISSION_NOTE.txt

Две-четыре недели без жалоб и без нового трафика в логах — разумный порог для большинства небольших и средних проектов. Для критичных систем с редкими, но важными интеграциями (партнёр обращается раз в квартал) этого может быть недостаточно — здесь решает не таймер, а знание бизнес-контекста; уточните у владельцев продукта, прежде чем полагаться только на отсутствие трафика. Общая методология такого решения — что можно гасить, а что держит бизнес — разобрана отдельно в статье «Что можно выключить, а что держит бизнес: ревизия сервисов на чужом сервере».

Практический порядок действий: от находки до безопасного удаления

Собрав всё вместе, порядок разбора трёх (или больше) версий одного приложения выглядит так: инвентаризация всех копий с портами и способом управления → сверка с активным proxy_pass/ProxyPass через nginx -T → подтверждение трафиком (логи с upstream_addr, tcpdump, свежесть логов приложения) → классификация некандидатов на «живая», «мёртвая», «непонятно» → для «непонятно» — остановка без удаления файлов и наблюдение разумный срок → и только после этого бэкап и фактическое удаление.

Финальный шаг — не удаляйте вслепую. Даже для копии, простоявшей остановленной месяц без единой жалобы, сначала архив каталога и дамп связанной базы (если она отдельная), и только потом rm -rf. Место на диске дешевле восстановления по памяти того, что там было:

tar -czf /backup/app_old_before_removal_$(date +%Y%m%d).tar.gz /opt/app_old
# и только после успешного архива
rm -rf /opt/app_old
systemctl disable app_old.service 2>/dev/null
rm -f /etc/systemd/system/app_old.service
systemctl daemon-reload

Отдельно уберите из nginx/Apache все упоминания удалённой копии, даже закомментированные — «мёртвый» proxy_pass, оставленный в конфиге «на всякий случай», через год снова превратится в загадку для следующего человека, который будет разбирать этот сервер.

Особый случай: обе версии отвечают на трафик одновременно

Иногда проверка не даёт чистого ответа «одна боевая, остальные мёртвые» — потому что трафик реально идёт на обе. Такое встречается в нескольких легитимных сценариях, и здесь торопиться с выводами особенно опасно:

  • Балансировка между инстансами. Если это не «версия A» и «версия B», а два одинаковых инстанса одной версии за load balancer'ом для отказоустойчивости — удалять один нельзя категорически, это половина продакшена. Проверьте upstream-блок nginx или HAProxy на предмет нескольких адресов в одной группе.
  • Постепенный canary-релиз. Небольшой процент трафика сознательно направлен на новую версию перед полным переключением — ищите веса (weight=) в блоке upstream или отдельный слой canary-маршрутизации на контейнерах.
  • Разделение по типу клиента. Мобильное приложение старой версии всё ещё обращается к старому эндпоинту, потому что часть пользователей не обновилась. Здесь «заброшенная» версия обслуживает живых людей, и её отключение сломает им работу. User-Agent в access-логе почти всегда выдаёт этот сценарий сразу.

Если после всех проверок картина осталась двусмысленной — это повод не удалять ничего, а восстановить историю: кто и когда осознанно менял конфигурацию маршрутизации. Это разобрано в статье «Кто и когда последний раз правил конфиги: восстанавливаем историю изменений».

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли просто остановить подозрительную копию на пару минут и посмотреть, кто пожалуется?

Рискованно для чего угодно, кроме заведомо тестового окружения. Двух минут недостаточно, если сервис дергается раз в день внешней интеграцией — жалоба придёт через сутки, когда вы уже забудете, что что-то останавливали. Лучше наблюдение по логам за несколько дней, короткую остановку — только когда логи уже подтвердили отсутствие трафика.

Что если ни одна из версий не совпадает с портом в конфиге веб-сервера?

Значит, либо есть ещё один слой между веб-сервером и приложением (внутренний балансировщик, второй reverse proxy), либо конфиг не тот, что реально обслуживает домен — проверьте, не смотрит ли DNS на другой сервер или не стоит ли перед nginx внешний CDN/прокси.

Версии отличаются несовместимой схемой базы данных — как понять, какая база к какой относится?

Проверьте .env/config каждой копии на имя базы, хоста и пользователя, затем сравните схему (SHOW TABLES / \dt) с тем, что ожидает код версии. Если совпадений нет ни с одной базой на сервере, вероятно, она вообще на другом хосте.

Стоит ли сразу переименовывать боевой каталог во что-то очевидное вроде app_production?

Да, но переименование каталога, к которому привязаны systemd-юнит и конфиг веб-сервера, требует правки всех этих мест синхронно, иначе вы создадите ещё одну версию несоответствия. Делайте это отдельным шагом после того, как всё стабилизировалось.

Как не допустить повторения этой ситуации в будущем?

Заведите привычку: тестовая копия получает явную домен-метку (test-, staging-) и запись в инвентаре сервера с датой и причиной создания — даже если это «просто на вечер». Через год «просто на вечер» превращается ровно в ту же загадку.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →