Skip to content

Runbook — GitLab self-hosted: бэкап и восстановление

Audience: operator / CTO. Закрывает «известный пробел» из runbook бэкапов: резервная копия данных самого инстанса GitLab (БД, issues, MR, CI-конфиг, registry, LFS, артефакты), а не только исходников.

GitLab: EE 18.10.3 Omnibus, хост gitlab-host (192.168.1.100, gitlab.local.lan). Доступ только через worker на home LAN: ssh worker 'ssh debian@192.168.1.100 "<cmd>"' (passwordless sudo).

Что бэкапится и куда

АртефактЧто внутриИсточникOff-host (worker)Off-site (aruba1)
*_gitlab_backup.tarБД, repositories, uploads (registry/artifacts/builds исключены, см. ниже)gitlab-backup create SKIP=…/var/opt/gitlab/backups//home/debian/gitlab-backups/*.tar.age (зашифрован) в backups@:~/gitlab-offsite/
gitlab-secrets.jsonключи шифрования (2FA, CI-vars, токены, runner-токены)/etc/gitlab/gitlab-secrets.json/home/debian/gitlab-backups/secrets/gitlab-secrets.json.age (зашифрован)
gitlab.rbконфигурация инстанса/etc/gitlab/gitlab.rb/home/debian/gitlab-backups/secrets/gitlab.rb.age (зашифрован)

gitlab-backup create НЕ включает gitlab-secrets.json и gitlab.rb. Без gitlab-secrets.json восстановленный инстанс мёртв: 2FA, CI-переменные, токены, ключи runner'ов нерасшифруемы. Поэтому секреты выводятся отдельными файлами (0600), их содержимое никогда не печатается в логи/транскрипты.

Что исключено из tar (shrink — registry)

gitlab-backup-run.sh запускается с SKIP=registry,artifacts,builds (override через env BACKUP_SKIP). Container registry — это ~5.6 ГБ из ~5.8 ГБ полного бэкапа и пересобираемо (CI заново пушит образы), поэтому исключено. Это сжимает tar до ~31 МБ (доказано: 6 094 243 840 → 30 853 120 байт) и разгружает gitlab-host (только ~6 ГБ свободно), worker /home (общий CI-диск) и off-site-передачу. Сохраняется незаменимое состояние: db, repositories, uploads. Registry/artifacts при необходимости просто заново пушатся/пересобираются CI.

Топология: три ноги (две на LAN + одна off-site)

  1. gitlab-host (.100) — авторитетный create + 1-дневный on-host tar.
  2. worker (.101, home LAN, отдельный диск) — off-host PULL + sha256-проверка.
  3. aruba1 (94.177.204.91, датацентр Италия) — настоящая географическая off-site нога: worker пушит age-зашифрованную копию (#78 follow-up). Раньше обе копии были на одном home LAN — теперь 3-2-1 закрыт географически.

Расписание и retention

ГдеЮнитКогдаRetention
gitlab-hostgitlab-backup.timergitlab-backup.service/usr/local/sbin/gitlab-backup-run.shежедневно 02:30 (+rand 5 мин)1 день (backup_keep_time); pre-prune перед каждым create
workergitlab-backup-pull.timergitlab-backup-pull.service/home/debian/gitlab-backups/pull-gitlab-backup.shежедневно 03:30 (+rand 10 мин)1 последний tar (диск /home = общий CI-диск)
worker → aruba1gitlab-backup-offsite.timergitlab-backup-offsite.service/home/debian/gitlab-backups/push-offsite.shежедневно 04:00 (+rand 10 мин), после pull2 последних age-зашифрованных набора на aruba1 (33 ГБ свободно)

Механизм off-host (worker-side PULL): worker по своему ключу тянет из debian-овладаемого staging-каталога gitlab-host:/home/debian/gitlab-backup-stage/. Pull-модель выбрана сознательно — на gitlab-host НЕ кладётся ключ worker'а.

gitlab-backup-run.sh (на gitlab-host, root через timer)

  1. gitlab-backup create CRON=1 — fail-closed (ненулевой exit → выход 1, виден в journalctl).
  2. Берёт свежайший tar; падает, если его нет/пустой.
  3. Обновляет /home/debian/gitlab-backup-stage/ (debian-owned, 0600): latest tar + копии обоих секретов.
  4. Пишет SHA256SUMS (только хэши, не содержимое) для off-host-проверки.

pull-gitlab-backup.sh (на worker, debian через timer)

  1. rsync tar + оба секрета + SHA256SUMS из staging gitlab-host → /home/debian/gitlab-backups/.
  2. Сверяет sha256 tar с записанным на gitlab-host — fail-closed при расхождении.
  3. Прунит старые tar off-host, оставляя KEEP=1.

push-offsite.sh (на worker, debian через timer) — off-site нога

  1. Берёт свежий проверенный off-host tar + оба секрета.
  2. age-шифрует tar и оба секрета публичным ключом worker:~/.git-key.pub (наружу уходит только ciphertext *.age).
  3. scp ciphertext + sha256-sidecar на backups@94.177.204.91:~/gitlab-offsite/ (non-root, 0700) через .partial→atomic rename.
  4. Сверяет sha256 на дальней стороне (sha256sum -c) — fail-closed.
  5. Retention KEEP=2 наборов tar.age на aruba1, прунит старее.

⚠ aruba1 хостит kontinuum-node + veil — мы пишем только в non-root каталог backups@:~/gitlab-offsite (0700), у worker'а нет root на aruba1.

Скрипты и юниты версионируются в kontinuum-infra (ops/gitlab-backup/). Идемпотентны и безопасны к повторному запуску.

Проверка (что всё живо)

bash
# таймеры заряжены
ssh worker 'ssh debian@192.168.1.100 "systemctl list-timers gitlab-backup.timer --no-pager"'
ssh worker 'systemctl list-timers gitlab-backup-pull.timer --no-pager'
# последний прогон на gitlab-host
ssh worker 'ssh debian@192.168.1.100 "sudo tail -20 /var/log/gitlab-backup-run.log"'
# off-host наличие + размер
ssh worker 'ls -la /home/debian/gitlab-backups /home/debian/gitlab-backups/secrets'
# off-host лог pull
ssh worker 'tail -20 /home/debian/gitlab-backups/pull.log'
# off-site таймер + лог push + содержимое на aruba1
ssh worker 'systemctl list-timers gitlab-backup-offsite.timer --no-pager'
ssh worker 'tail -20 /home/debian/gitlab-backups/push-offsite.log'
ssh worker 'ssh backups@94.177.204.91 "ls -la ~/gitlab-offsite"'

Проверка целостности tar (без распаковки)

bash
ssh worker 'ssh debian@192.168.1.100 "sudo tar -xOf /var/opt/gitlab/backups/<tar> backup_information.yml | grep -E \"gitlab_version|db_version|skipped\""'
# :gitlab_version должна совпадать с текущей при restore;
# :skipped ОЖИДАЕМО = registry,artifacts,builds (см. shrink выше) — это норма, не повреждение.

ВОССТАНОВЛЕНИЕ (на чистом / том же инстансе)

Жёсткое правило версии: restore делается на GitLab той же версии, что в backup_information.yml (:gitlab_version). Сейчас — 18.10.3-ee. Сначала довести Omnibus до этой версии, только потом restore.

Порядок обязателен: секреты ДО gitlab-backup restore. Иначе данные восстановятся, но будут нерасшифруемы.

Источник восстановления: off-host (worker) ИЛИ off-site (aruba1)

  • Обычный случай — берём открытые файлы с worker'а (/home/debian/gitlab-backups/ + secrets/).
  • Катастрофа (LAN/worker недоступны) — берём зашифрованные наборы с aruba1 и расшифровываем приватным ключом. ⚠ Приватный ключ ~/.git-key НЕ лежит на aruba1 (намеренно: ciphertext + ключ на одном публичном хосте = риск). Ключ — на worker (~/.git-key) + в менеджере паролей founder'а (out-of-band). Без этого ключа off-site копия нерасшифруема.
    bash
    # на машине, где есть ~/.git-key (worker / восстановленный из менеджера паролей)
    scp backups@94.177.204.91:gitlab-offsite/'*.age' .
    age -d -i ~/.git-key gitlab-secrets.json.age          > gitlab-secrets.json
    age -d -i ~/.git-key gitlab.rb.age                    > gitlab.rb
    age -d -i ~/.git-key <ts>_gitlab_backup.tar.age       > <ts>_gitlab_backup.tar
    # sanity: совпадение размера/sha256 с sidecar <ts>_gitlab_backup.tar.SHA256SUMS
  1. Поставить Omnibus GitLab той же версии (18.10.3-ee).
  2. Сначала секреты + конфиг (из off-host /home/debian/gitlab-backups/secrets/):
    bash
    sudo install -m 600 -o root -g root gitlab-secrets.json /etc/gitlab/gitlab-secrets.json
    sudo install -m 600 -o root -g root gitlab.rb            /etc/gitlab/gitlab.rb
    sudo gitlab-ctl reconfigure
  3. Положить tar в /var/opt/gitlab/backups/ (владелец git:git, 0600) и остановить пишущие сервисы:
    bash
    sudo cp <tar> /var/opt/gitlab/backups/ && sudo chown git:git /var/opt/gitlab/backups/<tar>
    sudo gitlab-ctl stop puma
    sudo gitlab-ctl stop sidekiq
  4. Restore (BACKUP= = timestamp-префикс имени файла, без _gitlab_backup.tar):
    bash
    sudo gitlab-backup restore BACKUP=1782246789_2026_06_23_18.10.3-ee
  5. Перезапуск + проверки:
    bash
    sudo gitlab-ctl reconfigure
    sudo gitlab-ctl restart
    sudo gitlab-rake gitlab:check SANITIZE=true
    sudo gitlab-rake gitlab:doctor:secrets   # подтверждает, что секреты расшифровывают данные
  6. Проверить health: curl -s -o /dev/null -w '%{http_code}' http://localhost/-/health200.

Восстановление одного git-репозитория

Код избыточно реплицирован в клонах worker/head — для потери одного репо обычно достаточно git push из клона, а не полного restore.

⚠ Крон-джевел

gitlab-secrets.json, gitlab.rb, любые токены/ключи — только файлами, 0600, содержимое никогда не печатать. Off-host (worker) хранятся в открытом виде (0600); off-site (aruba1) — только в age-зашифрованном виде (*.age). Приватный age-ключ ~/.git-key — на worker (0600) + менеджер паролей founder'а, не на aruba1.

Форки для CTO/founder

  • РЕШЕНО (#78 follow-up): off-site нога на aruba1. Worker пушит age-зашифрованную копию на aruba1 (датацентр Италия), retention=2; см. таблицу расписания. worker остаётся «горячей» открытой копией (добавили, не заменили).
  • РЕШЕНО: шифрование-at-rest off-site секретов. Off-site секреты И tar age-шифруются перед вывозом за пределы LAN. Приватный ключ намеренно НЕ кладётся на aruba1 (co-location ciphertext+ключа на публичном хосте свёл бы шифрование на нет при компрометации aruba1).
    • 🔸 Открытый под-форк для founder: если хотите, чтобы off-site копия была самодостаточной (расшифровываемой без worker'а/менеджера паролей), можно положить приватный ключ и на aruba1 — но это снижает ценность шифрования. По умолчанию: ключ не на aruba1.