Три версии одного приложения на машине: какую из них трогать нельзя
Заходите в /var/www или /opt на унаследованном сервере — и вместо одного приложения видите три: app, app_new, app2024. Или три отдельных systemd-юнита с почти одинаковыми именами, каждый слушает свой порт. Все три теоретически рабочие: запускаются, отвечают на запросы, ничего не падает при systemctl status. Вопрос «какую из них можно удалить» звучит просто, а ответ на глаз не виден — угадаете неправильно, и через десять минут в саппорт посыплются жалобы, что сайт не открывается. Разберём, как по трафику, логам и конфигу веб-сервера точно определить боевую версию, прежде чем трогать хоть один каталог.
Содержание
- Почему на одном сервере оказывается несколько версий одного приложения
- Первый шаг: найти все копии, а не только очевидные
- Порт в конфиге веб-сервера — самый быстрый и надёжный признак
- Трафик и логи обращений: кто реально отвечает пользователям
- Заброшенные копии: как отличить staging от «забыли выключить»
- Практический порядок действий: от находки до безопасного удаления
- Особый случай: обе версии отвечают на трафик одновременно
Почему на одном сервере оказывается несколько версий одного приложения
Дублирование почти никогда не результат злого умысла — это накопленный след нормальной, но недокументированной работы. Несколько типичных сценариев, которые сходятся к одному итогу:
- Тестовая копия для проверки обновления. Кто-то скопировал прод в
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 ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →