Апгрейд GitLab и два сигнала, которые врали
Рядовой минорный апгрейд self-hosted GitLab в Docker. Предварительная проверка насчитала 32 незавершённые миграции, а контейнер стал healthy, ничего не обслуживая.
В домашней лаборатории у меня GitLab в Docker. Это источник правды для всего моего GitOps, поэтому даже минорный апгрейд (19.1.2 → 19.2.2) я не делаю между делом. Это была плановая работа, не инцидент, но две вещи едва её не сорвали — и обе выглядели противоположностью того, чем были.
Сигнал первый: 32 незавершённые миграции
GitLab не даст обновиться, пока выполняются batched background migrations. Мой предварительный запрос показал 32 незавершённые миграции — это был бы жёсткий блокер.
Ошибка измерения. У колонки status два терминальных значения: 3 — finished и 6 — finalized. Фильтр status != 3 считает каждую финализированную миграцию незавершённой. Документированный критерий — status NOT IN (3, 6), и по нему реальное число было ноль. Попутно: прямой запрос в Postgres отвечал мгновенно, а аналогичный вызов через gitlab-rails runner занимал больше двух минут.
Сигнал второй: healthy за 60 секунд
После перезапуска Docker отметил контейнер как healthy примерно через минуту. Он не обслуживал ничего.
Health check образа покрывает узкую часть стека. Chef сначала поднимает Postgres, Redis, Gitaly и ещё несколько компонентов, прогоняет миграции схемы и только потом запускает веб-слой. Поэтому healthy сработал, когда Puma, Nginx, Workhorse и Sidekiq ещё не существовали. Когда Nginx всё же стартовал, он две минуты отдавал 502 — так что даже проверка TCP-порта отчиталась бы об успехе посреди простоя.
Процедура
Я шёл по фиксированному порядку: проверить → сделать бэкап → скачать → подменить → применить → проверить.
- Логический бэкап через
gitlab-backup createплюс архив каталога конфигурации, который бэкап намеренно не включает (там лежитgitlab-secrets.json). - Копия бэкапа за пределами хоста, сверенная
sha256sumс обеих сторон. docker pullнового образа на 3,9 ГБ до остановки старого контейнера — это удержало простой в 5 минутах 59 секундах против прогноза в 10–25 минут.- Замена закреплённого тега в compose-файле,
docker compose down && up -d. - Ожидание настоящего маркера завершения в логах, а не «Chef Client finished».
И принимал результат я только по видимым пользователю сигналам: страница входа отвечает 200, ssh -T git@… здоровается. Не по флагу здоровья от вендора.
Почему бэкап уезжает с хоста первым
Откат несимметричен. До того как новый контейнер прогнал миграции, откат занимает секунды: вернуть старый compose-файл и запустить старый образ. После миграций откатить тег нельзя: 19.2 переписывает схему, и 19.1 её не прочитает. Единственный путь назад — полное восстановление внутри старого образа, а это часы. Именно из-за этой асимметрии бэкап покидает хост до скачивания.
Выводы
- Знайте терминальные состояния того, что считаете.
!= finished— не то же самое, чтоnot in (finished, finalized). - Health check контейнера — мнение вендора. Принимайте апгрейд по тому, что видят пользователи: HTTP 200 и рабочий git через SSH.
- Скачивайте до остановки. Простой — это перезапуск, а не загрузка.
- Соблюдайте путь обновления. У GitLab есть обязательные остановки; остановка — потолок, а не цель: любая версия ниже следующей остановки достижима за один шаг.
- Никогда не запускайте такие апгрейды без присмотра. Cron, который применяет миграции базы, пока никто не смотрит, — тихий способ потерять выходные.