Sis admin linux

Версия от 21:52, 28 сентября 2026; Artem (обсуждение | вклад)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)

Небольшие инструкции, полезные команды и практические приёмы администрирования Linux.

Все IP-адреса, имена и идентификаторы приведены для примера. Заменяйте их значениями своей инфраструктуры.

SSH-доступ с macOS к LXC-контейнеру Proxmox по ключу

Назначение

Подключение с Mac к LXC-контейнеру по SSH без ввода пароля root.

Публичный ключ добавляется через консоль Proxmox. Знать или создавать пароль root контейнера для этого не требуется.

Параметр Значение для примера
ID контейнера 123
IP-адрес контейнера 192.168.2.2
Пользователь SSH root
Клиент Mac с macOS
Тип ключа Ed25519

Что необходимо

  • Административный доступ к консоли узла Proxmox.
  • Запущенный LXC-контейнер с работающим SSH-сервером.
  • Сетевая доступность контейнера с Mac — через локальную сеть или VPN.
  • Разрешённый на SSH-сервере вход root по публичному ключу.

1. Создание ключа на Mac

Открыть Терминал на Mac и выполнить:

if [ ! -f ~/.ssh/id_ed25519 ]; then
  ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519
else
  echo "SSH-ключ уже существует."
fi

Существующий ключ не перезаписывается.

При создании нового ключа появляются два файла:

Файл Назначение
~/.ssh/id_ed25519 Приватный ключ. Остаётся на Mac. Его нельзя публиковать или передавать.
~/.ssh/id_ed25519.pub Публичный ключ. Добавляется на сервер.

Важно: параметр -N "" создаёт ключ без парольной фразы. Получивший приватный ключ сможет использовать его для подключения к серверам, где разрешён соответствующий публичный ключ.

Если нужна защита ключа парольной фразой, при отсутствии ключа вместо команды выше выполнить:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519

Парольную фразу такого ключа можно сохранить в связке ключей macOS — порядок приведён ниже.

2. Копирование публичного ключа

На Mac выполнить:

cat ~/.ssh/id_ed25519.pub

Скопировать всю выведенную строку, начинающуюся с ssh-ed25519.

Нужен именно публичный ключ:

  • Копировать строку целиком, без добавления переносов.
  • Не копировать отпечаток SHA256:....
  • Не копировать графический рисунок ключа.
  • Не передавать содержимое приватного файла id_ed25519.

Если приватный ключ существует, но файл .pub отсутствует, восстановить публичный ключ:

ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub

3. Вход в контейнер через Proxmox

В веб-интерфейсе Proxmox открыть Shell узла, на котором запущен контейнер.

Выполнить, заменив 123 фактическим ID контейнера:

pct enter 123

Следующие команды выполняются внутри контейнера от имени root.

4. Добавление ключа в контейнер

Подготовить каталог и файл:

mkdir -p /root/.ssh
touch /root/.ssh/authorized_keys
chown root:root /root/.ssh /root/.ssh/authorized_keys
chmod 700 /root/.ssh
chmod 600 /root/.ssh/authorized_keys

В следующей команде заменить ВСТАВЬ_ПУБЛИЧНЫЙ_КЛЮЧ скопированной строкой с Mac, сохранив одинарные кавычки:

printf '\n%s\n' 'ВСТАВЬ_ПУБЛИЧНЫЙ_КЛЮЧ' >> /root/.ssh/authorized_keys

Оператор >> добавляет запись в конец файла и сохраняет ранее добавленные ключи.

Начальный перенос строки не позволяет новому ключу склеиться с предыдущей записью. Пустые строки в файле допустимы.

Перезапуск SSH после добавления ключа не требуется.

5. Проверка подлинности сервера

При первом подключении Mac может запросить подтверждение ключа сервера.

Чтобы узнать отпечаток ключа ED25519, выполнить внутри контейнера через консоль Proxmox:

ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

Сравнить отпечаток SHA256:... с тем, который покажет Mac при подключении.

Сравнивать нужно ключи одного типа: в этом примере ED25519.

6. Подключение с Mac

В Терминале на Mac выполнить:

ssh root@192.168.2.2

Заменить адрес на IP своего контейнера.

Если появился запрос подтверждения ключа сервера и отпечаток совпадает с проверенным, ввести:

yes

При успешной настройке откроется оболочка контейнера без запроса пароля root.

Для завершения подключения:

exit

7. Сохранение парольной фразы ключа в macOS

Этот шаг нужен только для ключа, защищённого парольной фразой.

Запрос Enter passphrase for key относится к приватному ключу на Mac, а не к паролю root контейнера.

На Mac выполнить:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

Ввести парольную фразу ключа.

Для автоматического использования связки ключей добавить настройки из следующего раздела.

8. Короткая команда подключения

Можно назначить подключению понятное имя, например my-container.

На Mac открыть файл настроек SSH:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/config

Добавить блок, заменив адрес на нужный:

Host my-container
    HostName 192.168.2.2
    User root
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
    AddKeysToAgent yes
    UseKeychain yes

Если блок Host my-container уже существует, изменить его вместо создания дубликата. Если в файле есть общий блок Host *, этот отдельный блок разместить перед ним.

Сохранить файл в nano:

  1. Нажать Control + O.
  2. Нажать Enter.
  3. Нажать Control + X.

Установить права:

chmod 600 ~/.ssh/config

Теперь подключение выполняется короткой командой:

ssh my-container

Диагностика

Запрашивается пароль root

Авторизация по ключу не завершилась успешно.

На Mac выполнить подключение с явным указанием ключа и подробным выводом:

ssh -v -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.2.2

Внутри контейнера через консоль Proxmox проверить:

ls -ld /root/.ssh
ls -l /root/.ssh/authorized_keys

Ожидаемые параметры:

Объект Права Владелец
/root/.ssh 700 — drwx------ root
/root/.ssh/authorized_keys 600 — -rw------- root

Проверить, что:

  • В файле находится публичный ключ с нужного Mac.
  • Ключ записан одной строкой.
  • Указаны правильные IP-адрес и пользователь.
  • SSH-сервер разрешает авторизацию по ключу и вход root по ключу.

Connection refused

Соединение отклонено.

Проверить правильность адреса, работу SSH-сервера и порт подключения. Также причиной может быть правило межсетевого экрана, отклоняющее соединение.

Connection timed out

Сервер не ответил за отведённое время.

Проверить:

  • Запущен ли контейнер.
  • Доступна ли его сеть с Mac.
  • Подключён ли нужный VPN.
  • Не блокируется ли SSH-порт межсетевым экраном.

REMOTE HOST IDENTIFICATION HAS CHANGED

Сохранённый на Mac ключ сервера отличается от текущего.

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

Сначала проверить новый отпечаток через доверенную консоль Proxmox. Не удалять старую запись без проверки причины изменения.

Примечания

  • Настройка сохраняется после перезагрузки Mac и контейнера.
  • Для каждого дополнительного компьютера можно создать собственный ключ и добавить его публичную часть отдельной строкой в authorized_keys.
  • Чтобы отозвать доступ конкретного ключа, удалить соответствующую строку из authorized_keys. Уже открытые SSH-сеансы при этом не завершаются.
  • Добавление ключа само по себе не отключает парольную авторизацию SSH.
  • Приватные ключи, пароли и другие секреты не должны попадать в публичную документацию.


Создание службы systemd: автозапуск и перезапуск программы

Что такое юнит

Юнит — объект, которым управляет systemd. Юнит с расширением .service описывает службу: какую программу запустить, под каким пользователем и что делать при её завершении.

Служба позволяет:

  • Запускать программу при загрузке системы.
  • Продолжать работу после отключения от SSH.
  • Автоматически перезапускать программу после сбоя.
  • Просматривать состояние и журналы стандартными командами.

Ниже приведён рабочий учебный пример для Debian и Ubuntu с systemd. Служба будет записывать сообщение в журнал каждые 30 секунд.

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

1. Создание пользователя службы

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

id demo-service >/dev/null 2>&1 || useradd \
  --system \
  --user-group \
  --home-dir /opt/demo-service \
  --no-create-home \
  --shell /usr/sbin/nologin \
  demo-service

Пользователь предназначен для запуска службы, а не для интерактивного входа.

Создать каталог программы:

install -d -o root -g root -m 755 /opt/demo-service

2. Создание программы для проверки

Открыть файл:

nano /opt/demo-service/run.sh

Вставить:

#!/bin/bash

while true; do
    printf '%s — служба работает\n' "$(date --iso-8601=seconds)"
    sleep 30
done

Сохранить: Ctrl + O, Enter, Ctrl + X.

Установить права:

chown root:root /opt/demo-service/run.sh
chmod 755 /opt/demo-service/run.sh

Пользователь службы сможет прочитать и запустить файл, но не изменить его.

3. Создание юнита

Открыть новый файл:

nano /etc/systemd/system/demo-service.service

Вставить:

[Unit]
Description=Example background service
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
Type=exec
User=demo-service
Group=demo-service
WorkingDirectory=/opt/demo-service
ExecStart=/bin/bash /opt/demo-service/run.sh
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Пояснение параметров

Параметр Назначение
[Unit] Описание службы, зависимости и ограничения запуска.
Description Название, которое отображается в состоянии службы.
StartLimitIntervalSec=60 Интервал учёта частых запусков — 60 секунд.
StartLimitBurst=5 Допускается до пяти запусков за указанный интервал. Ограничивает бесконечные попытки запуска при ошибке.
[Service] Параметры процесса.
Type=exec systemd ожидает успешного запуска исполняемого файла. Это ещё не означает, что приложение полностью готово обслуживать запросы.
User и Group Пользователь и группа процесса.
WorkingDirectory Рабочий каталог программы. Должен существовать и быть доступен пользователю службы.
ExecStart Команда запуска с абсолютными путями.
Restart=on-failure Перезапуск после ошибки или аварийного завершения. Штатное завершение с кодом 0 не вызывает перезапуск.
RestartSec=5 Пауза перед перезапуском — пять секунд.
StandardOutput=journal Обычный вывод программы направляется в журнал.
StandardError=journal Ошибки программы направляются в журнал.
[Install] Настройки включения автозапуска.
WantedBy=multi-user.target Позволяет включить службу в обычный многопользовательский режим загрузки.

4. Проверка и запуск

Проверить юнит:

systemd-analyze verify /etc/systemd/system/demo-service.service

Если обнаружены ошибки в созданном файле — исправить их до запуска.

Перечитать юниты, включить автозапуск и сразу запустить службу:

systemctl daemon-reload
systemctl enable --now demo-service.service

Посмотреть состояние:

systemctl status demo-service.service --no-pager -l

Ожидаемое состояние:

Active: active (running)

5. Просмотр журнала

Последние 50 записей:

journalctl -u demo-service.service -n 50 --no-pager

Наблюдение за новыми записями:

journalctl -u demo-service.service -f

Нажатие Ctrl + C завершает просмотр журнала, но не останавливает службу.

Основные команды управления

Команда Действие
systemctl start demo-service Запустить.
systemctl stop demo-service Остановить. Настройка Restart не отменяет ручную остановку.
systemctl restart demo-service Перезапустить.
systemctl status demo-service Посмотреть состояние.
systemctl enable demo-service Включить автозапуск, не запуская службу немедленно.
systemctl disable demo-service Отключить автозапуск, не останавливая текущий процесс.
systemctl disable --now demo-service Отключить автозапуск и остановить службу.
systemctl cat demo-service Показать юнит и дополнительные настройки.

Как адаптировать под свою программу

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

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

WorkingDirectory=/opt/my-app
ExecStart=/opt/my-app/.venv/bin/python -u /opt/my-app/main.py

Виртуальное окружение и файл программы должны существовать. Активировать окружение командой source не требуется.

Параметр -u отключает буферизацию стандартных потоков Python, чтобы сообщения быстрее попадали в журнал.

Особенности:

  • Программа должна работать на переднем плане: не добавляйте &, nohup или собственный переход в фон.
  • ExecStart не является обычной командной строкой оболочки: конструкции |, > и && не выполняются как в Bash.
  • Сложную последовательность команд удобнее оформить отдельным скриптом.
  • Пользователю службы нужны права чтения программы и записи только в необходимые каталоги данных.
  • Переменные из интерактивного SSH-сеанса автоматически в службу не передаются.

Для программы, которой при запуске нужна настроенная сеть, в секцию [Unit] можно добавить:

Wants=network-online.target
After=network-online.target

Это не гарантирует доступность Интернета или внешнего API. Приложение должно самостоятельно повторять неудачные подключения.

Применение изменений

После изменения юнита:

systemd-analyze verify /etc/systemd/system/demo-service.service
systemctl daemon-reload
systemctl restart demo-service.service
systemctl status demo-service.service --no-pager -l

Если изменён только код программы, обычно достаточно перезапуска службы.

Если сработало ограничение частых запусков, сначала исправить причину по журналу, затем выполнить:

systemctl reset-failed demo-service.service
systemctl start demo-service.service


Отключение парольного входа по SSH

Назначение

Разрешить новые SSH-подключения только по публичному ключу.

Имя пользователя по-прежнему указывается, например root@192.168.2.2. Меняется способ подтверждения доступа.

Настройка не удаляет системные пароли и не меняет правила входа через локальную консоль или использования sudo.

Пример предназначен для Debian и Ubuntu с OpenSSH. Команды сервера выполняются от имени root.

1. Проверка входа по ключу

Не закрывайте текущий сеанс до завершения всех проверок. Держите доступной консоль Proxmox или другую независимую консоль сервера.

В новом терминале Mac проверить вход с запретом перехода к парольной авторизации:

ssh \
  -o ControlMaster=no \
  -o ControlPath=none \
  -o PreferredAuthentications=publickey \
  -o PasswordAuthentication=no \
  -o KbdInteractiveAuthentication=no \
  -o IdentitiesOnly=yes \
  -i ~/.ssh/id_ed25519 \
  root@192.168.2.2

Если вход не работает, сначала исправить авторизацию по ключу.

Если сервером пользуются несколько администраторов, заранее настроить ключи для каждого.

2. Резервная копия конфигурации

На сервере выполнить:

cp -a /etc/ssh/sshd_config \
  "/etc/ssh/sshd_config.backup-$(date +%Y%m%d-%H%M%S)"

3. Изменение настроек

Открыть:

nano /etc/ssh/sshd_config

Добавить в самое начало файла, перед директивами Include и блоками Match:

PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey
Параметр Значение
PubkeyAuthentication yes Разрешить авторизацию по публичному ключу.
PasswordAuthentication no Запретить обычную парольную авторизацию.
KbdInteractiveAuthentication no Запретить интерактивную авторизацию, которая также может запрашивать пароль или одноразовый код.
AuthenticationMethods publickey Оставить только авторизацию по публичному ключу.

Этот вариант также исключает другие способы входа и схемы «ключ плюс одноразовый код».

Для большинства параметров OpenSSH используется первое полученное значение. Блоки Match могут задавать отдельные правила для пользователей и адресов, поэтому нужна проверка итоговой конфигурации.

Параметр PermitRootLogin без необходимости не менять. Значение no запрещает root любой SSH-вход; prohibit-password допускает вход root по ключу.

4. Проверка перед применением

Проверить синтаксис:

/usr/sbin/sshd -t

Отсутствие вывода при успешном завершении означает, что проверка пройдена. При ошибке конфигурацию не применять.

Посмотреть общие итоговые параметры:

/usr/sbin/sshd -T | grep -E '^(pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|authenticationmethods) '

Ожидаемые значения:

pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no
authenticationmethods publickey

При наличии Match дополнительно проверить конкретное подключение:

/usr/sbin/sshd -T \
  -C user=root,addr=192.168.2.50,host=mac-client,laddr=192.168.2.2,lport=22 \
  | grep -E '^(pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|authenticationmethods|permitrootlogin) '

В примере:

  • user — пользователь сервера.
  • addr — адрес клиента, видимый серверу.
  • host — имя клиента для правил Match Host.
  • laddr и lport — адрес и порт сервера.

Подставить реальные параметры подключения. Проверить также другие учётные записи и адреса, для которых настроены отдельные правила.

Если значения отличаются от ожидаемых, найти и исправить соответствующие настройки в основном файле, подключаемых файлах или блоках Match.

5. Применение

Для стандартной службы SSH в Debian и Ubuntu:

/usr/sbin/sshd -t && systemctl reload ssh.service

В системах, где служба называется sshd.service, использовать:

/usr/sbin/sshd -t && systemctl reload sshd.service

Выполняется только вариант с фактическим именем службы.

Перечитывание конфигурации штатно сохраняет открытые сеансы.

6. Проверка нового подключения

Не закрывая старый сеанс, в новом терминале Mac повторить команду проверки входа по ключу из первого шага.

Затем проверить отказ для парольных способов:

ssh \
  -o ControlMaster=no \
  -o ControlPath=none \
  -o PubkeyAuthentication=no \
  -o PreferredAuthentications=password,keyboard-interactive \
  root@192.168.2.2

Ожидаемый результат — отказ, обычно:

Permission denied (publickey).

Пароль учётной записи запрашиваться не должен.

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

Восстановление при ошибке

Использовать оставленный открытым сеанс или консоль Proxmox.

Найти резервную копию:

ls -lt /etc/ssh/sshd_config.backup-*

Восстановить нужную копию, указав её фактическое имя:

cp -a /etc/ssh/sshd_config.backup-YYYYMMDD-HHMMSS /etc/ssh/sshd_config

Проверить и применить:

/usr/sbin/sshd -t && systemctl reload ssh.service

Если служба называется sshd.service, заменить её имя в команде.

Важно помнить

  • Парольная фраза приватного ключа на Mac может запрашиваться и после отключения паролей на сервере — это отдельная защита самого ключа.
  • Настройка распространяется также на SFTP и SCP через этот SSH-сервер.
  • Не удаляйте пароль пользователя и не блокируйте учётную запись ради отключения парольного SSH-входа: достаточно настроек SSH.

Проверка версии и обновление программы из GitLab

Назначение

Проверить, появились ли новые изменения в GitLab, сравнить их с установленным на сервере кодом и обновить программу.

Инструкция предназначена для приложения, которое уже установлено из Git-репозитория и запускается через systemd.

Все пути, имена и адреса приведены для примера.

Параметр Пример
Каталог программы /opt/my-app
Репозиторий git@gitlab.example.com:team/my-app.git
Удалённый репозиторий origin
Рабочая ветка main
Служба my-app.service

Команды Git выполнять от имени владельца рабочей копии. Для управления системной службой нужны права root или sudo.

Команды выполняются поэтапно. При ошибке не переходить к следующему шагу.

Основные понятия

Термин Значение
Коммит Зафиксированное состояние кода с уникальным идентификатором.
Ветка Последовательность разработки, например main или stable.
HEAD Текущий коммит рабочей копии.
origin Обычно так называется подключённый удалённый репозиторий.
origin/main Последнее полученное локально состояние ветки main из origin. До fetch сведения могут быть устаревшими.
Тег Именованная отметка версии, например v1.4.0.
Release Опубликованный выпуск в GitLab, обычно связанный с тегом и описанием изменений.

Новый коммит в ветке не обязательно означает новый стабильный выпуск. Перед установкой проверить назначение ветки, описание выпуска и результаты CI в GitLab.

1. Проверка каталога и службы

Перейти в каталог программы:

cd /opt/my-app

Убедиться, что открыт нужный репозиторий:

pwd
git rev-parse --show-toplevel
git remote -v

Проверить, откуда systemd запускает программу:

sudo systemctl show my-app.service --no-pager \
  -p WorkingDirectory \
  -p ExecStart \
  -p User

Пути должны соответствовать обновляемому приложению.

Если служба запускает Docker-контейнер, установленный пакет или собранный файл из другого каталога, одного обновления исходников недостаточно. Нужна предусмотренная проектом сборка и установка.

Не публикуйте вывод git remote -v, если в адрес репозитория встроен токен доступа.

2. Определение текущей версии

Выполнить:

git branch --show-current
git log -1 --format='%h %ci %s'
git rev-parse HEAD
git status --short --branch

Команды показывают:

  • Текущую ветку.
  • Последний коммит, дату и описание.
  • Полный идентификатор коммита.
  • Состояние рабочей копии.

Если имя ветки не выводится, репозиторий находится в состоянии detached HEAD — например, после установки определённого тега. Основной сценарий ниже рассчитан на ветку main.

Коммит в каталоге не доказывает, что запущенный процесс уже использует этот код. После обновления обычно требуется перезапуск или отдельный этап развёртывания.

3. Проверка локальных изменений

Выполнить:

git status --porcelain

Пустой вывод означает отсутствие отслеживаемых изменений и неотслеживаемых файлов. Игнорируемые файлы, например .env, эта команда не показывает.

Типичные обозначения:

Обозначение Значение
M Файл изменён.
A Новый файл добавлен в индекс.
D Файл удалён.
?? Файл не отслеживается Git.

Если есть изменения, сначала выяснить их назначение:

git diff
git diff --cached

Не удалять локальные правки ради обновления. Сохранить нужный код отдельно или оформить его в согласованную ветку разработки.

Не использовать git reset --hard или git clean -fd как универсальное исправление: они могут уничтожить нужные изменения и файлы.

4. Получение сведений из GitLab

Для примера с веткой main выполнить:

git fetch origin refs/heads/main:refs/remotes/origin/main

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

Если GitLab недоступен или авторизация завершилась ошибкой, остановиться: сравнение со старым origin/main не подтверждает актуальность сервера.

Для приватного репозитория на сервере нужен доступ на чтение, например отдельный SSH Deploy Key. Ключ для входа с Mac на сервер сам по себе не предоставляет серверу доступ к GitLab.

5. Сравнение сервера с GitLab

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

git rev-list --left-right --count HEAD...origin/main
Результат Значение Действие
0 0 История совпадает. Обновление кода не требуется.
0 3 В GitLab есть три новых коммита. Можно изучать обновление.
2 0 На сервере есть два коммита, которых нет в origin/main. Выяснить происхождение локальных коммитов.
2 3 Истории разошлись. Разобраться с ветками; обычное обновление не выполнять.

Первое число относится к HEAD сервера, второе — к origin/main.

Продолжать основной сценарий следует только при результате 0 N, где N больше нуля, и при текущей ветке main.

6. Просмотр состава обновления

Зафиксировать идентификатор проверяемой версии:

UPDATE_COMMIT=$(git rev-parse origin/main)
printf '%s\n' "$UPDATE_COMMIT"

Следующие шаги выполнять в том же терминале. После переподключения переменную нужно задать снова.

Посмотреть новые коммиты:

git --no-pager log --oneline HEAD.."$UPDATE_COMMIT"

Посмотреть список и объём изменений:

git --no-pager diff --stat HEAD "$UPDATE_COMMIT"
git --no-pager diff --name-status HEAD "$UPDATE_COMMIT"

Посмотреть изменения кода:

git diff HEAD "$UPDATE_COMMIT"

Для выхода из просмотра нажать q.

Перед установкой проверить:

  • Изменения зависимостей и требования к версии языка.
  • Новые настройки и переменные окружения.
  • Изменения юнита systemd.
  • Необходимость сборки.
  • Миграции базы данных.
  • Инструкцию обновления и совместимость со старой версией.

Обновляться будем до сохранённого UPDATE_COMMIT — именно до просмотренной версии.

7. Подготовка отката

Создать локальную ветку, сохраняющую текущий коммит:

BACKUP_BRANCH="backup/before-update-$(date +%Y%m%d-%H%M%S)"
git branch "$BACKUP_BRANCH" HEAD
printf 'Ветка для отката: %s\n' "$BACKUP_BRANCH"

Записать её имя.

Эта ветка сохраняет только версию кода в Git. Она не является резервной копией базы данных, загруженных файлов, .env или виртуального окружения.

До обновления отдельно обеспечить восстановление:

  • Конфигурации и секретов.
  • Базы данных штатными средствами используемой СУБД.
  • Пользовательских файлов.
  • Зависимостей приложения.

При миграциях заранее определить, совместима ли новая структура базы со старым кодом. Копирование файлов работающей базы обычной командой cp не заменяет корректную резервную копию.

8. Остановка и обновление кода

Этот способ предполагает короткий простой приложения.

Ещё раз проверить:

git branch --show-current
git status --porcelain

Ожидается ветка main и пустой вывод второй команды.

Остановить службу:

sudo systemctl stop my-app.service
sudo systemctl is-active my-app.service

Ожидаемый ответ — inactive. Для неактивной службы команда is-active возвращает ненулевой код, это нормально.

Если приложение запускается также таймером, сокетом или внешним монитором, на время обновления приостановить и этот механизм.

Обновить рабочую копию:

git merge --ff-only "$UPDATE_COMMIT"

Параметр --ff-only разрешает только прямое продвижение по истории без создания merge-коммита. При расхождении веток команда откажется выполнять слияние.

При ошибке не продолжать установку и не запускать непроверенную версию.

9. Зависимости, сборка и проверки

Выполнить шаги, предусмотренные документацией проекта. Универсальной команды установки для всех программ нет.

Например, для Python-проекта, использующего виртуальное окружение и requirements.txt:

.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m pip check

Для проверки синтаксиса Python, если исходники находятся в каталоге app:

.venv/bin/python -m compileall -q app

Заменить каталог app реальным каталогом исходников.

Проверка синтаксиса не заменяет тестирование функций программы. Если проект использует lock-файл или другой менеджер зависимостей, применять его штатную процедуру.

При ошибке установки, сборки, тестов или миграции остановиться.

Миграции базы выполнять только по инструкции проекта и после резервного копирования.

Если изменился установленный юнит systemd, проверить и применить его отдельно:

sudo systemd-analyze verify /etc/systemd/system/my-app.service
sudo systemctl daemon-reload

Изменение копии юнита в Git-репозитории само по себе не обновляет файл в /etc/systemd/system.

10. Запуск и проверка

После успешного выполнения подготовительных шагов запустить приложение:

sudo systemctl start my-app.service
sudo systemctl status my-app.service --no-pager -l

Посмотреть свежие журналы:

sudo journalctl -u my-app.service --since "5 minutes ago" --no-pager

Сравнить установленный коммит с выбранным:

git rev-parse HEAD
printf '%s\n' "$UPDATE_COMMIT"

Идентификаторы должны совпасть.

Проверить программу по назначению:

  • Для бота — отправить тестовую команду.
  • Для сайта — открыть страницу и проверить основную операцию.
  • Для API — выполнить предусмотренную проверку состояния.
  • Для фоновой службы — убедиться, что она обрабатывает задания.

Состояние active (running) подтверждает наличие процесса, но не гарантирует работоспособность приложения.

11. Откат к предыдущему коду

Откат выполнять только после оценки совместимости старого кода с текущей базой и зависимостями.

Остановить службу:

sudo systemctl stop my-app.service

Найти сохранённую ветку:

git branch --list 'backup/before-update-*'
git status --porcelain

При наличии новых локальных изменений сначала сохранить и изучить их.

Переключиться на фактическую резервную ветку:

git switch backup/before-update-YYYYMMDD-HHMMSS

Подставить имя из списка. Команда меняет рабочие файлы на сохранённую версию, не переписывая ветку main.

Затем:

  1. Восстановить совместимые зависимости и конфигурацию.
  2. При необходимости выполнить предусмотренное восстановление базы.
  3. Проверить приложение.
  4. Запустить службу и посмотреть журнал.
sudo systemctl start my-app.service
sudo systemctl status my-app.service --no-pager -l
sudo journalctl -u my-app.service -n 50 --no-pager

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

Повторная установка старого requirements.txt не гарантирует точное восстановление окружения: новые лишние пакеты могут остаться, а незакреплённые версии — измениться.

Если новая версия опубликована тегом

Если проект выпускает стабильные версии тегами, ориентироваться на указанный в GitLab Release тег, а не на последний коммит main.

Получить теги:

git fetch origin --tags
git tag --list --sort=-version:refname

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

Для выбранного примера:

git show --no-patch v1.4.0
git diff --stat HEAD v1.4.0
git log --oneline HEAD..v1.4.0

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

git switch --detach v1.4.0

Это отдельный вариант установки. После него выполнить зависимости, сборку и проверки согласно выпуску.

Состояние detached HEAD здесь ожидаемо: сервер закреплён за конкретным коммитом версии.

Частые ошибки

not a git repository

Команда выполнена не в каталоге репозитория:

cd /opt/my-app
git status

Permission denied (publickey) при fetch

GitLab не принял SSH-ключ сервера.

Проверить адрес origin и доступ серверного ключа к нужному проекту. Не переносить приватный ключ с личного компьютера без необходимости.

Your local changes would be overwritten

На сервере есть изменения файлов, которым мешает обновление.

Посмотреть git status и git diff. Не применять принудительный сброс без понимания, какие данные будут потеряны.

Not possible to fast-forward

Истории разошлись. Проверить:

git log --oneline --graph --decorate --max-count=30 HEAD origin/main

Разобраться с локальными коммитами и изменениями ветки в GitLab до обновления.

Код обновлён, поведение старое

Проверить:

  • Был ли перезапущен нужный процесс.
  • Из какого каталога запускается служба.
  • Не требуется ли сборка.
  • Не работает ли другая копия приложения.
  • Не запускается ли приложение из Docker-образа или установленного пакета.

Короткая проверка наличия обновлений

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

(
  cd /opt/my-app || exit 1

  git status --short --branch
  git log -1 --oneline

  git fetch origin refs/heads/main:refs/remotes/origin/main || exit 1

  git rev-list --left-right --count HEAD...origin/main
  git --no-pager log --oneline HEAD..origin/main
  git --no-pager diff --stat HEAD origin/main
)

Автоматическое обновление программы из GitLab каждые 3 минуты

Назначение

Сервер периодически проверяет выбранную ветку GitLab и устанавливает новые коммиты.

Если изменений нет, программа продолжает работать без перезапуска.

При наличии обновления выполняются:

  1. Проверка состояния репозитория.
  2. Проверка допустимости изменений.
  3. Сохранение предыдущего коммита.
  4. Остановка приложения.
  5. Обновление кода.
  6. Проверка синтаксиса Python.
  7. Запуск и проверка работоспособности.
  8. Попытка отката при ошибке.

Пример предназначен для Debian и Ubuntu с systemd.

Область применения

Этот вариант обновляет код Python-приложения в существующем каталоге. Во время установки будет короткий простой.

Он не устанавливает зависимости, не выполняет миграции базы, не пересобирает Docker-образы и не меняет системные настройки.

Изменения за пределами разрешённых файлов требуют ручного обновления.

Откат кода не отменяет изменения данных, которые успела сделать программа. Автоматизировать таким способом следует только совместимые обновления без изменения формата данных и обязательных миграций.

Пример конфигурации

Параметр Значение
Каталог приложения /opt/my-app
Владелец репозитория myapp
Ветка для установки main
Удалённый репозиторий origin
Служба приложения my-app.service
Виртуальное окружение /opt/my-app/.venv
Исходники Python app/ и необязательный main.py
Проверка состояния http://127.0.0.1:8000/health
Интервал 3 минуты после завершения предыдущей проверки

Все значения условные. Пользователь myapp, репозиторий, виртуальное окружение и служба должны уже существовать.

Ветка для автоматической установки должна содержать только одобренные изменения. В GitLab следует ограничить прямую запись в неё и настроить прохождение CI перед слиянием.

Сам скрипт не проверяет статус pipeline GitLab: публикация коммита в выбранной ветке считается разрешением на установку.

1. Подготовка

Команды настройки выполнять от имени root на сервере приложения.

Установить необходимые инструменты:

apt update
apt install -y git curl util-linux

Проверить службу:

systemctl show my-app.service --no-pager \
  -p User \
  -p WorkingDirectory \
  -p ExecStart

systemctl status my-app.service --no-pager -l

Она должна запускать программу из обновляемого каталога. Само приложение желательно запускать под отдельным непривилегированным пользователем.

Проверить репозиторий от имени его владельца:

runuser -u myapp -- git -C /opt/my-app status --short --branch
runuser -u myapp -- git -C /opt/my-app branch --show-current

Ожидается ветка main без локальных изменений и неотслеживаемых файлов.

Логи, виртуальное окружение и локальные настройки должны находиться вне отслеживаемого кода либо быть корректно исключены через .gitignore.

2. Доступ сервера к GitLab

У пользователя myapp должен быть настроен доступ на чтение репозитория.

Для SSH можно использовать отдельный Deploy Key с правами только на чтение. Ключ сервера GitLab должен быть заранее проверен и сохранён в known_hosts этого пользователя.

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

runuser -u myapp -- env \
  GIT_TERMINAL_PROMPT=0 \
  GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=10" \
  git -C /opt/my-app ls-remote --exit-code origin refs/heads/main

Команда должна вывести идентификатор ветки и завершиться успешно.

Не отключать проверку ключа SSH-сервера ради автоматизации.

3. Проверка состояния приложения

В примере приложение предоставляет адрес:

http://127.0.0.1:8000/health

Проверить:

curl --noproxy '*' --max-time 5 \
  -sS -o /dev/null -w '%{http_code}\n' \
  http://127.0.0.1:8000/health

Ожидается HTTP 200.

Этот endpoint должен существовать в приложении и отражать его готовность к работе. Не подставляйте произвольную страницу только потому, что она отвечает кодом 200.

Для Telegram-бота без HTTP endpoint функцию проверки в скрипте нужно заменить реальной проверкой готовности бота. Одного состояния active недостаточно.

4. Создание скрипта обновления

Открыть:

nano /usr/local/sbin/my-app-update

Вставить:

#!/bin/bash
set -Eeuo pipefail

export PATH=/usr/sbin:/usr/bin:/sbin:/bin

APP_DIR="/opt/my-app"
APP_USER="myapp"
APP_SERVICE="my-app.service"
BRANCH="main"
REMOTE="origin"
HEALTH_URL="http://127.0.0.1:8000/health"

STATE_DIR="/var/lib/my-app-updater"
PAUSE_FILE="$STATE_DIR/PAUSED"

old_commit=""
new_commit=""
deployment_started=0

log() {
    printf '%s %s\n' "$(date --iso-8601=seconds)" "$*"
}

as_app() {
    runuser -u "$APP_USER" -- "$@"
}

git_app() {
    as_app env \
      GIT_TERMINAL_PROMPT=0 \
      GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=10" \
      git -C "$APP_DIR" "$@"
}

health_once() {
    local http_code

    systemctl is-active --quiet "$APP_SERVICE" || return 1

    http_code=$(
        as_app curl --noproxy '*' \
          --connect-timeout 2 --max-time 5 \
          -sS -o /dev/null -w '%{http_code}' \
          "$HEALTH_URL"
    ) || return 1

    [[ "$http_code" == "200" ]]
}

wait_healthy() {
    local attempt

    for attempt in {1..12}; do
        if health_once; then
            return 0
        fi
        sleep 5
    done

    return 1
}

finish() {
    local result=$?
    trap - EXIT
    set +e

    if (( result != 0 && deployment_started == 1 )); then
        log "Ошибка установки. Пытаемся вернуть $old_commit."
        log "Автообновление останется на паузе."

        if systemctl stop "$APP_SERVICE"; then
            if git_app switch --no-overwrite-ignore \
                --detach "$old_commit"; then

                if systemctl start "$APP_SERVICE" && wait_healthy; then
                    log "Предыдущий код восстановлен, проверка пройдена."
                else
                    log "КРИТИЧНО: после отката приложение не прошло проверку."
                fi
            else
                log "КРИТИЧНО: вернуть код не удалось. Нужна ручная проверка."
            fi
        else
            log "КРИТИЧНО: остановка не удалась; файлы не переключаем."
        fi
    fi

    exit "$result"
}

trap finish EXIT

install -d -m 700 "$STATE_DIR"

exec 9>/run/lock/my-app-update.lock
if ! flock -n 9; then
    log "Другой запуск уже выполняется."
    exit 0
fi

if [[ -e "$PAUSE_FILE" ]]; then
    log "Автообновление на паузе: $PAUSE_FILE"
    exit 0
fi

cd "$APP_DIR"

current_branch=$(git_app branch --show-current)
if [[ "$current_branch" != "$BRANCH" ]]; then
    log "Ожидалась ветка $BRANCH, получена: $current_branch"
    exit 1
fi

working_changes=$(git_app status --porcelain --untracked-files=all)
if [[ -n "$working_changes" ]]; then
    log "Есть локальные изменения или неотслеживаемые файлы."
    exit 1
fi

# Если приложение было остановлено администратором,
# обновлятор не должен сам запускать его.
if ! systemctl is-active --quiet "$APP_SERVICE"; then
    log "Приложение не активно. Обновление пропущено."
    exit 0
fi

log "Проверяем $REMOTE/$BRANCH."

as_app env \
  GIT_TERMINAL_PROMPT=0 \
  GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=10" \
  timeout 60 git -C "$APP_DIR" fetch "$REMOTE" \
  "refs/heads/$BRANCH:refs/remotes/$REMOTE/$BRANCH"

old_commit=$(git_app rev-parse HEAD)
new_commit=$(git_app rev-parse "refs/remotes/$REMOTE/$BRANCH")

if [[ "$old_commit" == "$new_commit" ]]; then
    log "Новых коммитов нет."
    exit 0
fi

if ! git_app merge-base --is-ancestor "$old_commit" "$new_commit"; then
    log "Истории разошлись или удалённая ветка отстаёт."
    exit 1
fi

# Разрешены только перечисленные пути.
# Зависимости, конфигурация, юниты и прочее требуют ручной установки.
changed_files=$(git_app diff --no-renames --name-only \
    "$old_commit" "$new_commit")

while IFS= read -r changed_path; do
    case "$changed_path" in
        migrations/*|*/migrations/*|alembic/*|*/alembic/*)
            log "Миграции требуют ручного обновления."
            exit 1
            ;;
        app/*.py|main.py|tests/*|docs/*|README.md)
            ;;
        *)
            log "Есть изменения вне разрешённых путей. Нужна ручная установка."
            exit 1
            ;;
    esac
done <<< "$changed_files"

# Не начинать установку поверх уже неисправного приложения.
if ! health_once; then
    log "Текущая версия не проходит проверку состояния."
    exit 1
fi

# Повторная проверка перед изменением файлов.
working_changes=$(git_app status --porcelain --untracked-files=all)
if [[ -n "$working_changes" ]] ||
   [[ "$(git_app rev-parse HEAD)" != "$old_commit" ]]; then
    log "Рабочая копия изменилась во время проверки."
    exit 1
fi

backup_branch="backup/auto-$(date +%Y%m%d-%H%M%S)"
git_app branch "$backup_branch" "$old_commit"

printf 'old=%s\nnew=%s\nbackup=%s\n' \
    "$old_commit" "$new_commit" "$backup_branch" > "$PAUSE_FILE"

# Маркер удаляется только после успешного обновления.
# При обрыве или перезагрузке следующий запуск не продолжит установку.
deployment_started=1

log "Устанавливаем $new_commit. Резервная ветка: $backup_branch"

systemctl stop "$APP_SERVICE"

if systemctl is-active --quiet "$APP_SERVICE"; then
    log "Приложение не остановилось."
    exit 1
fi

git_app merge --ff-only --no-overwrite-ignore "$new_commit"

# Проверка синтаксиса без создания файлов __pycache__.
# Пути нужно адаптировать под структуру проекта.
as_app "$APP_DIR/.venv/bin/python" - "$APP_DIR" <<'PY'
import pathlib
import sys

root = pathlib.Path(sys.argv[1])
files = list((root / "app").rglob("*.py"))

entry = root / "main.py"
if entry.is_file():
    files.append(entry)

if not files:
    raise SystemExit("Не найдены исходники для проверки")

for path in files:
    compile(path.read_bytes(), str(path), "exec")

print(f"Проверен синтаксис файлов: {len(files)}")
PY

systemctl start "$APP_SERVICE"

if ! wait_healthy; then
    log "Новая версия не прошла проверку состояния."
    exit 1
fi

rm -- "$PAUSE_FILE"
deployment_started=0
log "Обновление завершено: $new_commit"

Установить владельца, права и проверить синтаксис Bash:

chown root:root /usr/local/sbin/my-app-update
chmod 750 /usr/local/sbin/my-app-update
bash -n /usr/local/sbin/my-app-update

Отсутствие вывода при успешном завершении bash -n означает, что синтаксис Bash корректен. Это не проверка работоспособности всей процедуры.

Как устроена защита

Механизм Назначение
flock Не позволяет двум экземплярам скрипта одновременно менять приложение.
git status Останавливает обновление при локальных изменениях.
fetch с ограничением времени Проверяет GitLab без остановки программы.
merge --ff-only Отказывается объединять разошедшиеся истории.
--no-overwrite-ignore Не разрешает перезаписывать мешающие обновлению игнорируемые файлы.
Разрешённые пути Отправляет изменения зависимостей и конфигурации на ручную установку.
Резервная ветка Сохраняет ссылку на предыдущий коммит.
Проверка синтаксиса и HTTP Проверяет код и ответ запущенного приложения.
Файл PAUSED Блокирует дальнейшие установки после ошибки или прерванного обновления.

Git, Python и HTTP-проверка выполняются от имени myapp. Root используется для управления службой и служебными файлами обновлятора.

Список разрешённых путей не анализирует смысл кода. Даже изменение одного Python-файла может оказаться несовместимым с данными. Поэтому проверка изменений и CI перед публикацией в рабочую ветку обязательны.

Во время работы обновлятора не выполнять ручные Git-операции в этом каталоге. Блокировка flock действует только на процессы, которые используют тот же файл блокировки.

5. Создание службы обновления

Открыть:

nano /etc/systemd/system/my-app-update.service

Вставить:

[Unit]
Description=Update my-app from GitLab
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=root
UMask=0077
ExecStart=/usr/local/sbin/my-app-update
TimeoutStartSec=15min
StandardOutput=journal
StandardError=journal

Это отдельная служба обновлятора. Существующую my-app.service заменять не нужно.

TimeoutStartSec ограничивает длительность всей операции. При принудительном завершении откат не гарантирован: файл PAUSED сохраняется для ручного восстановления.

6. Создание таймера

Открыть:

nano /etc/systemd/system/my-app-update.timer

Вставить:

[Unit]
Description=Check my-app updates every 3 minutes

[Timer]
OnBootSec=2min
OnUnitInactiveSec=3min
AccuracySec=10s
Unit=my-app-update.service

[Install]
WantedBy=timers.target

Параметры:

  • OnBootSec=2min — первая проверка через две минуты после загрузки. При включении таймера позднее этого момента она может начаться сразу.
  • OnUnitInactiveSec=3min — следующая проверка через три минуты после завершения предыдущего запуска.
  • AccuracySec=10s — допустимое окно срабатывания таймера.

Для другого интервала заменить:

OnUnitInactiveSec=2min

или:

OnUnitInactiveSec=5min

Это интервал между проверками, а не гарантированный срок установки: сама операция тоже занимает время.

7. Первый запуск вручную

До включения расписания адаптировать пути, пользователя, ветку и healthcheck.

Проверить файлы:

systemd-analyze verify \
  /etc/systemd/system/my-app-update.service \
  /etc/systemd/system/my-app-update.timer

systemctl daemon-reload

Запустить одну проверку:

systemctl start my-app-update.service
journalctl -u my-app-update.service -n 100 --no-pager

Этот запуск уже устанавливает новые коммиты, если они есть. Выполнять в согласованное время обслуживания.

Сначала проверить весь сценарий на тестовой копии, включая успешное обновление и отказ проверки с откатом.

После успешного выполнения oneshot-служба обычно имеет состояние inactive (dead). Для неё это нормально.

Результат последнего запуска:

systemctl show my-app-update.service \
  -p Result \
  -p ExecMainStatus

Ожидается:

Result=success
ExecMainStatus=0

8. Включение расписания

После проверки включить таймер:

systemctl enable --now my-app-update.timer
systemctl list-timers --all my-app-update.timer

В таблице будут показаны последнее и следующее срабатывания.

Просмотр журналов

Журнал обновлятора:

journalctl -u my-app-update.service -n 100 --no-pager

Наблюдение в реальном времени:

journalctl -u my-app-update.service -f

Журнал самого приложения:

journalctl -u my-app.service -n 100 --no-pager

Пример штатной проверки:

Проверяем origin/main.
Новых коммитов нет.

Изменение интервала

Изменить OnUnitInactiveSec в файле таймера, затем выполнить:

systemctl daemon-reload
systemctl restart my-app-update.timer
systemctl list-timers --all my-app-update.timer

Временное отключение

Остановить расписание и отключить его автозапуск:

systemctl disable --now my-app-update.timer

Это не останавливает приложение и не прерывает уже выполняющееся обновление.

Перед ручными работами проверить:

systemctl status my-app-update.service --no-pager

Дождаться завершения текущей операции.

Что делать после ошибки обновления

Если ошибка произошла после начала установки, скрипт пытается вернуть предыдущий код. Дальнейшие установки блокируются файлом:

/var/lib/my-app-updater/PAUSED

Отключить таймер и изучить состояние:

systemctl disable --now my-app-update.timer

cat /var/lib/my-app-updater/PAUSED

journalctl -u my-app-update.service -n 150 --no-pager
systemctl status my-app.service --no-pager -l

runuser -u myapp -- git -C /opt/my-app status
runuser -u myapp -- git -C /opt/my-app log -1 --oneline

После успешного отката репозиторий находится в detached HEAD на старом коммите. Ветка main может по-прежнему указывать на неудачное обновление.

Не переключаться обратно на main и не удалять PAUSED вслепую.

Для восстановления:

  1. Исправить или отменить проблемное изменение в GitLab.
  2. Вручную установить и проверить исправленную версию по процедуре ручного обновления.
  3. Убедиться, что текущая ветка main, рабочая копия чистая и приложение исправно.
  4. Только после этого снять паузу.
rm /var/lib/my-app-updater/PAUSED
systemctl reset-failed my-app-update.service
systemctl enable --now my-app-update.timer

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

Изменение зависимостей или базы данных

Если в GitLab изменились requirements.txt, lock-файл, юнит, конфигурация или файлы миграций, обновлятор откажется устанавливать такой коммит.

В этом случае:

  1. Отключить таймер.
  2. Дождаться завершения обновлятора.
  3. Сделать необходимые резервные копии.
  4. Выполнить ручное обновление по инструкции проекта.
  5. Проверить приложение.
  6. Снова включить таймер.

Не добавлять автоматическую установку зависимостей или миграций без отдельного плана восстановления.

Ограничения

  • Резервная ветка сохраняет только код, а не базу и пользовательские файлы.
  • Успешный HTTP 200 не заменяет полноценные функциональные тесты.
  • Скрипт не поддерживает развёртывание Git LFS и submodules без дополнительной адаптации.
  • Приложение не должно самостоятельно менять отслеживаемые файлы.
  • На время обновления не должно быть внешнего механизма, который снова запускает остановленную службу.
  • Ошибки записываются в журнал; уведомления администратору здесь не настроены.
  • Резервные ветки постепенно накапливаются. Их удаление следует выполнять отдельно после проверки стабильности и наличия резервных копий.