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)
- gitlab-host (
.100) — авторитетный create + 1-дневный on-host tar. - worker (
.101, home LAN, отдельный диск) — off-host PULL + sha256-проверка. - aruba1 (
94.177.204.91, датацентр Италия) — настоящая географическая off-site нога: worker пушит age-зашифрованную копию (#78 follow-up). Раньше обе копии были на одном home LAN — теперь 3-2-1 закрыт географически.
Расписание и retention
| Где | Юнит | Когда | Retention |
|---|---|---|---|
| gitlab-host | gitlab-backup.timer → gitlab-backup.service → /usr/local/sbin/gitlab-backup-run.sh | ежедневно 02:30 (+rand 5 мин) | 1 день (backup_keep_time); pre-prune перед каждым create |
| worker | gitlab-backup-pull.timer → gitlab-backup-pull.service → /home/debian/gitlab-backups/pull-gitlab-backup.sh | ежедневно 03:30 (+rand 10 мин) | 1 последний tar (диск /home = общий CI-диск) |
| worker → aruba1 | gitlab-backup-offsite.timer → gitlab-backup-offsite.service → /home/debian/gitlab-backups/push-offsite.sh | ежедневно 04:00 (+rand 10 мин), после pull | 2 последних 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)
gitlab-backup create CRON=1— fail-closed (ненулевой exit → выход 1, виден вjournalctl).- Берёт свежайший tar; падает, если его нет/пустой.
- Обновляет
/home/debian/gitlab-backup-stage/(debian-owned, 0600): latest tar + копии обоих секретов. - Пишет
SHA256SUMS(только хэши, не содержимое) для off-host-проверки.
pull-gitlab-backup.sh (на worker, debian через timer)
rsynctar + оба секрета +SHA256SUMSиз staging gitlab-host →/home/debian/gitlab-backups/.- Сверяет sha256 tar с записанным на gitlab-host — fail-closed при расхождении.
- Прунит старые tar off-host, оставляя
KEEP=1.
push-offsite.sh (на worker, debian через timer) — off-site нога
- Берёт свежий проверенный off-host tar + оба секрета.
- age-шифрует tar и оба секрета публичным ключом
worker:~/.git-key.pub(наружу уходит только ciphertext*.age). scpciphertext + sha256-sidecar наbackups@94.177.204.91:~/gitlab-offsite/(non-root, 0700) через.partial→atomic rename.- Сверяет sha256 на дальней стороне (
sha256sum -c) — fail-closed. - Retention
KEEP=2наборов tar.age на aruba1, прунит старее.
⚠ aruba1 хостит kontinuum-node + veil — мы пишем только в non-root каталог
backups@:~/gitlab-offsite(0700), у worker'а нет root на aruba1.
Скрипты и юниты версионируются в kontinuum-infra (
ops/gitlab-backup/). Идемпотентны и безопасны к повторному запуску.
Проверка (что всё живо)
# таймеры заряжены
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 (без распаковки)
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
- Поставить Omnibus GitLab той же версии (18.10.3-ee).
- Сначала секреты + конфиг (из off-host
/home/debian/gitlab-backups/secrets/):bashsudo 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 - Положить tar в
/var/opt/gitlab/backups/(владелецgit:git, 0600) и остановить пишущие сервисы:bashsudo 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 - Restore (BACKUP= = timestamp-префикс имени файла, без
_gitlab_backup.tar):bashsudo gitlab-backup restore BACKUP=1782246789_2026_06_23_18.10.3-ee - Перезапуск + проверки:bash
sudo gitlab-ctl reconfigure sudo gitlab-ctl restart sudo gitlab-rake gitlab:check SANITIZE=true sudo gitlab-rake gitlab:doctor:secrets # подтверждает, что секреты расшифровывают данные - Проверить health:
curl -s -o /dev/null -w '%{http_code}' http://localhost/-/health→200.
Восстановление одного 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.