Sis admin linux: различия между версиями
Artem (обсуждение | вклад) Нет описания правки |
Artem (обсуждение | вклад) Нет описания правки |
||
| (не показаны 2 промежуточные версии этого же участника) | |||
| Строка 304: | Строка 304: | ||
Описание и команды. | Описание и команды. | ||
--> | --> | ||
== Создание службы systemd: автозапуск и перезапуск программы == | == Создание службы systemd: автозапуск и перезапуск программы == | ||
| Строка 781: | Строка 782: | ||
* Настройка распространяется также на SFTP и SCP через этот SSH-сервер. | * Настройка распространяется также на SFTP и SCP через этот SSH-сервер. | ||
* Не удаляйте пароль пользователя и не блокируйте учётную запись ради отключения парольного SSH-входа: достаточно настроек SSH. | * Не удаляйте пароль пользователя и не блокируйте учётную запись ради отключения парольного SSH-входа: достаточно настроек SSH. | ||
== Проверка версии и обновление программы из GitLab == | |||
=== Назначение === | |||
Проверить, появились ли новые изменения в GitLab, сравнить их с установленным на сервере кодом и обновить программу. | |||
Инструкция предназначена для приложения, которое уже установлено из Git-репозитория и запускается через systemd. | |||
'''Все пути, имена и адреса приведены для примера.''' | |||
{| class="wikitable" | |||
! Параметр !! Пример | |||
|- | |||
| Каталог программы || <code>/opt/my-app</code> | |||
|- | |||
| Репозиторий || <code>git@gitlab.example.com:team/my-app.git</code> | |||
|- | |||
| Удалённый репозиторий || <code>origin</code> | |||
|- | |||
| Рабочая ветка || <code>main</code> | |||
|- | |||
| Служба || <code>my-app.service</code> | |||
|} | |||
Команды Git выполнять от имени владельца рабочей копии. Для управления системной службой нужны права root или sudo. | |||
'''Команды выполняются поэтапно. При ошибке не переходить к следующему шагу.''' | |||
=== Основные понятия === | |||
{| class="wikitable" | |||
! Термин !! Значение | |||
|- | |||
| Коммит | |||
| Зафиксированное состояние кода с уникальным идентификатором. | |||
|- | |||
| Ветка | |||
| Последовательность разработки, например <code>main</code> или <code>stable</code>. | |||
|- | |||
| HEAD | |||
| Текущий коммит рабочей копии. | |||
|- | |||
| origin | |||
| Обычно так называется подключённый удалённый репозиторий. | |||
|- | |||
| origin/main | |||
| Последнее полученное локально состояние ветки main из origin. До fetch сведения могут быть устаревшими. | |||
|- | |||
| Тег | |||
| Именованная отметка версии, например <code>v1.4.0</code>. | |||
|- | |||
| Release | |||
| Опубликованный выпуск в GitLab, обычно связанный с тегом и описанием изменений. | |||
|} | |||
'''Новый коммит в ветке не обязательно означает новый стабильный выпуск.''' Перед установкой проверить назначение ветки, описание выпуска и результаты CI в GitLab. | |||
=== 1. Проверка каталога и службы === | |||
Перейти в каталог программы: | |||
<syntaxhighlight lang="bash"> | |||
cd /opt/my-app | |||
</syntaxhighlight> | |||
Убедиться, что открыт нужный репозиторий: | |||
<syntaxhighlight lang="bash"> | |||
pwd | |||
git rev-parse --show-toplevel | |||
git remote -v | |||
</syntaxhighlight> | |||
Проверить, откуда systemd запускает программу: | |||
<syntaxhighlight lang="bash"> | |||
sudo systemctl show my-app.service --no-pager \ | |||
-p WorkingDirectory \ | |||
-p ExecStart \ | |||
-p User | |||
</syntaxhighlight> | |||
Пути должны соответствовать обновляемому приложению. | |||
Если служба запускает Docker-контейнер, установленный пакет или собранный файл из другого каталога, одного обновления исходников недостаточно. Нужна предусмотренная проектом сборка и установка. | |||
'''Не публикуйте вывод git remote -v, если в адрес репозитория встроен токен доступа.''' | |||
=== 2. Определение текущей версии === | |||
Выполнить: | |||
<syntaxhighlight lang="bash"> | |||
git branch --show-current | |||
git log -1 --format='%h %ci %s' | |||
git rev-parse HEAD | |||
git status --short --branch | |||
</syntaxhighlight> | |||
Команды показывают: | |||
* Текущую ветку. | |||
* Последний коммит, дату и описание. | |||
* Полный идентификатор коммита. | |||
* Состояние рабочей копии. | |||
Если имя ветки не выводится, репозиторий находится в состоянии detached HEAD — например, после установки определённого тега. Основной сценарий ниже рассчитан на ветку main. | |||
'''Коммит в каталоге не доказывает, что запущенный процесс уже использует этот код.''' После обновления обычно требуется перезапуск или отдельный этап развёртывания. | |||
=== 3. Проверка локальных изменений === | |||
Выполнить: | |||
<syntaxhighlight lang="bash"> | |||
git status --porcelain | |||
</syntaxhighlight> | |||
Пустой вывод означает отсутствие отслеживаемых изменений и неотслеживаемых файлов. Игнорируемые файлы, например .env, эта команда не показывает. | |||
Типичные обозначения: | |||
{| class="wikitable" | |||
! Обозначение !! Значение | |||
|- | |||
| <code>M</code> | |||
| Файл изменён. | |||
|- | |||
| <code>A</code> | |||
| Новый файл добавлен в индекс. | |||
|- | |||
| <code>D</code> | |||
| Файл удалён. | |||
|- | |||
| <code>??</code> | |||
| Файл не отслеживается Git. | |||
|} | |||
Если есть изменения, сначала выяснить их назначение: | |||
<syntaxhighlight lang="bash"> | |||
git diff | |||
git diff --cached | |||
</syntaxhighlight> | |||
Не удалять локальные правки ради обновления. Сохранить нужный код отдельно или оформить его в согласованную ветку разработки. | |||
'''Не использовать git reset --hard или git clean -fd как универсальное исправление: они могут уничтожить нужные изменения и файлы.''' | |||
=== 4. Получение сведений из GitLab === | |||
Для примера с веткой main выполнить: | |||
<syntaxhighlight lang="bash"> | |||
git fetch origin refs/heads/main:refs/remotes/origin/main | |||
</syntaxhighlight> | |||
Команда получает актуальное состояние нужной ветки. Файлы программы в рабочем каталоге не обновляются, служба продолжает работать. | |||
Если GitLab недоступен или авторизация завершилась ошибкой, остановиться: сравнение со старым origin/main не подтверждает актуальность сервера. | |||
Для приватного репозитория на сервере нужен доступ на чтение, например отдельный SSH Deploy Key. Ключ для входа с Mac на сервер сам по себе не предоставляет серверу доступ к GitLab. | |||
=== 5. Сравнение сервера с GitLab === | |||
Показать количество уникальных коммитов с каждой стороны: | |||
<syntaxhighlight lang="bash"> | |||
git rev-list --left-right --count HEAD...origin/main | |||
</syntaxhighlight> | |||
{| class="wikitable" | |||
! Результат !! Значение !! Действие | |||
|- | |||
| <code>0 0</code> | |||
| История совпадает. | |||
| Обновление кода не требуется. | |||
|- | |||
| <code>0 3</code> | |||
| В GitLab есть три новых коммита. | |||
| Можно изучать обновление. | |||
|- | |||
| <code>2 0</code> | |||
| На сервере есть два коммита, которых нет в origin/main. | |||
| Выяснить происхождение локальных коммитов. | |||
|- | |||
| <code>2 3</code> | |||
| Истории разошлись. | |||
| Разобраться с ветками; обычное обновление не выполнять. | |||
|} | |||
Первое число относится к HEAD сервера, второе — к origin/main. | |||
'''Продолжать основной сценарий следует только при результате 0 N, где N больше нуля, и при текущей ветке main.''' | |||
=== 6. Просмотр состава обновления === | |||
Зафиксировать идентификатор проверяемой версии: | |||
<syntaxhighlight lang="bash"> | |||
UPDATE_COMMIT=$(git rev-parse origin/main) | |||
printf '%s\n' "$UPDATE_COMMIT" | |||
</syntaxhighlight> | |||
Следующие шаги выполнять в том же терминале. После переподключения переменную нужно задать снова. | |||
Посмотреть новые коммиты: | |||
<syntaxhighlight lang="bash"> | |||
git --no-pager log --oneline HEAD.."$UPDATE_COMMIT" | |||
</syntaxhighlight> | |||
Посмотреть список и объём изменений: | |||
<syntaxhighlight lang="bash"> | |||
git --no-pager diff --stat HEAD "$UPDATE_COMMIT" | |||
git --no-pager diff --name-status HEAD "$UPDATE_COMMIT" | |||
</syntaxhighlight> | |||
Посмотреть изменения кода: | |||
<syntaxhighlight lang="bash"> | |||
git diff HEAD "$UPDATE_COMMIT" | |||
</syntaxhighlight> | |||
Для выхода из просмотра нажать <code>q</code>. | |||
Перед установкой проверить: | |||
* Изменения зависимостей и требования к версии языка. | |||
* Новые настройки и переменные окружения. | |||
* Изменения юнита systemd. | |||
* Необходимость сборки. | |||
* Миграции базы данных. | |||
* Инструкцию обновления и совместимость со старой версией. | |||
Обновляться будем до сохранённого UPDATE_COMMIT — именно до просмотренной версии. | |||
=== 7. Подготовка отката === | |||
Создать локальную ветку, сохраняющую текущий коммит: | |||
<syntaxhighlight lang="bash"> | |||
BACKUP_BRANCH="backup/before-update-$(date +%Y%m%d-%H%M%S)" | |||
git branch "$BACKUP_BRANCH" HEAD | |||
printf 'Ветка для отката: %s\n' "$BACKUP_BRANCH" | |||
</syntaxhighlight> | |||
Записать её имя. | |||
'''Эта ветка сохраняет только версию кода в Git.''' Она не является резервной копией базы данных, загруженных файлов, .env или виртуального окружения. | |||
До обновления отдельно обеспечить восстановление: | |||
* Конфигурации и секретов. | |||
* Базы данных штатными средствами используемой СУБД. | |||
* Пользовательских файлов. | |||
* Зависимостей приложения. | |||
При миграциях заранее определить, совместима ли новая структура базы со старым кодом. Копирование файлов работающей базы обычной командой cp не заменяет корректную резервную копию. | |||
=== 8. Остановка и обновление кода === | |||
Этот способ предполагает короткий простой приложения. | |||
Ещё раз проверить: | |||
<syntaxhighlight lang="bash"> | |||
git branch --show-current | |||
git status --porcelain | |||
</syntaxhighlight> | |||
Ожидается ветка main и пустой вывод второй команды. | |||
Остановить службу: | |||
<syntaxhighlight lang="bash"> | |||
sudo systemctl stop my-app.service | |||
sudo systemctl is-active my-app.service | |||
</syntaxhighlight> | |||
Ожидаемый ответ — <code>inactive</code>. Для неактивной службы команда is-active возвращает ненулевой код, это нормально. | |||
Если приложение запускается также таймером, сокетом или внешним монитором, на время обновления приостановить и этот механизм. | |||
Обновить рабочую копию: | |||
<syntaxhighlight lang="bash"> | |||
git merge --ff-only "$UPDATE_COMMIT" | |||
</syntaxhighlight> | |||
Параметр <code>--ff-only</code> разрешает только прямое продвижение по истории без создания merge-коммита. При расхождении веток команда откажется выполнять слияние. | |||
'''При ошибке не продолжать установку и не запускать непроверенную версию.''' | |||
=== 9. Зависимости, сборка и проверки === | |||
Выполнить шаги, предусмотренные документацией проекта. Универсальной команды установки для всех программ нет. | |||
Например, для Python-проекта, использующего виртуальное окружение и requirements.txt: | |||
<syntaxhighlight lang="bash"> | |||
.venv/bin/python -m pip install -r requirements.txt | |||
.venv/bin/python -m pip check | |||
</syntaxhighlight> | |||
Для проверки синтаксиса Python, если исходники находятся в каталоге app: | |||
<syntaxhighlight lang="bash"> | |||
.venv/bin/python -m compileall -q app | |||
</syntaxhighlight> | |||
Заменить каталог app реальным каталогом исходников. | |||
Проверка синтаксиса не заменяет тестирование функций программы. Если проект использует lock-файл или другой менеджер зависимостей, применять его штатную процедуру. | |||
'''При ошибке установки, сборки, тестов или миграции остановиться.''' | |||
Миграции базы выполнять только по инструкции проекта и после резервного копирования. | |||
Если изменился установленный юнит systemd, проверить и применить его отдельно: | |||
<syntaxhighlight lang="bash"> | |||
sudo systemd-analyze verify /etc/systemd/system/my-app.service | |||
sudo systemctl daemon-reload | |||
</syntaxhighlight> | |||
Изменение копии юнита в Git-репозитории само по себе не обновляет файл в /etc/systemd/system. | |||
=== 10. Запуск и проверка === | |||
После успешного выполнения подготовительных шагов запустить приложение: | |||
<syntaxhighlight lang="bash"> | |||
sudo systemctl start my-app.service | |||
sudo systemctl status my-app.service --no-pager -l | |||
</syntaxhighlight> | |||
Посмотреть свежие журналы: | |||
<syntaxhighlight lang="bash"> | |||
sudo journalctl -u my-app.service --since "5 minutes ago" --no-pager | |||
</syntaxhighlight> | |||
Сравнить установленный коммит с выбранным: | |||
<syntaxhighlight lang="bash"> | |||
git rev-parse HEAD | |||
printf '%s\n' "$UPDATE_COMMIT" | |||
</syntaxhighlight> | |||
Идентификаторы должны совпасть. | |||
Проверить программу по назначению: | |||
* Для бота — отправить тестовую команду. | |||
* Для сайта — открыть страницу и проверить основную операцию. | |||
* Для API — выполнить предусмотренную проверку состояния. | |||
* Для фоновой службы — убедиться, что она обрабатывает задания. | |||
'''Состояние active (running) подтверждает наличие процесса, но не гарантирует работоспособность приложения.''' | |||
=== 11. Откат к предыдущему коду === | |||
Откат выполнять только после оценки совместимости старого кода с текущей базой и зависимостями. | |||
Остановить службу: | |||
<syntaxhighlight lang="bash"> | |||
sudo systemctl stop my-app.service | |||
</syntaxhighlight> | |||
Найти сохранённую ветку: | |||
<syntaxhighlight lang="bash"> | |||
git branch --list 'backup/before-update-*' | |||
git status --porcelain | |||
</syntaxhighlight> | |||
При наличии новых локальных изменений сначала сохранить и изучить их. | |||
Переключиться на фактическую резервную ветку: | |||
<syntaxhighlight lang="bash"> | |||
git switch backup/before-update-YYYYMMDD-HHMMSS | |||
</syntaxhighlight> | |||
Подставить имя из списка. Команда меняет рабочие файлы на сохранённую версию, не переписывая ветку main. | |||
Затем: | |||
# Восстановить совместимые зависимости и конфигурацию. | |||
# При необходимости выполнить предусмотренное восстановление базы. | |||
# Проверить приложение. | |||
# Запустить службу и посмотреть журнал. | |||
<syntaxhighlight lang="bash"> | |||
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 | |||
</syntaxhighlight> | |||
'''После отката текущей становится резервная ветка.''' Перед следующим обновлением нужно осознанно выбрать дальнейшую ветку или выпуск. | |||
Повторная установка старого requirements.txt не гарантирует точное восстановление окружения: новые лишние пакеты могут остаться, а незакреплённые версии — измениться. | |||
=== Если новая версия опубликована тегом === | |||
Если проект выпускает стабильные версии тегами, ориентироваться на указанный в GitLab Release тег, а не на последний коммит main. | |||
Получить теги: | |||
<syntaxhighlight lang="bash"> | |||
git fetch origin --tags | |||
git tag --list --sort=-version:refname | |||
</syntaxhighlight> | |||
Порядок списка не гарантирует, что первый тег — рекомендованный стабильный выпуск. Проверить описание релиза и отметку предварительной версии в GitLab. | |||
Для выбранного примера: | |||
<syntaxhighlight lang="bash"> | |||
git show --no-patch v1.4.0 | |||
git diff --stat HEAD v1.4.0 | |||
git log --oneline HEAD..v1.4.0 | |||
</syntaxhighlight> | |||
После просмотра, резервного копирования и остановки службы вместо обновления ветки можно установить конкретный тег: | |||
<syntaxhighlight lang="bash"> | |||
git switch --detach v1.4.0 | |||
</syntaxhighlight> | |||
Это отдельный вариант установки. После него выполнить зависимости, сборку и проверки согласно выпуску. | |||
Состояние detached HEAD здесь ожидаемо: сервер закреплён за конкретным коммитом версии. | |||
=== Частые ошибки === | |||
==== not a git repository ==== | |||
Команда выполнена не в каталоге репозитория: | |||
<syntaxhighlight lang="bash"> | |||
cd /opt/my-app | |||
git status | |||
</syntaxhighlight> | |||
==== Permission denied (publickey) при fetch ==== | |||
GitLab не принял SSH-ключ сервера. | |||
Проверить адрес origin и доступ серверного ключа к нужному проекту. Не переносить приватный ключ с личного компьютера без необходимости. | |||
==== Your local changes would be overwritten ==== | |||
На сервере есть изменения файлов, которым мешает обновление. | |||
Посмотреть git status и git diff. Не применять принудительный сброс без понимания, какие данные будут потеряны. | |||
==== Not possible to fast-forward ==== | |||
Истории разошлись. Проверить: | |||
<syntaxhighlight lang="bash"> | |||
git log --oneline --graph --decorate --max-count=30 HEAD origin/main | |||
</syntaxhighlight> | |||
Разобраться с локальными коммитами и изменениями ветки в GitLab до обновления. | |||
==== Код обновлён, поведение старое ==== | |||
Проверить: | |||
* Был ли перезапущен нужный процесс. | |||
* Из какого каталога запускается служба. | |||
* Не требуется ли сборка. | |||
* Не работает ли другая копия приложения. | |||
* Не запускается ли приложение из Docker-образа или установленного пакета. | |||
=== Короткая проверка наличия обновлений === | |||
Следующий блок только получает сведения и сравнивает версии. Он не обновляет рабочие файлы и не перезапускает программу: | |||
<syntaxhighlight lang="bash"> | |||
( | |||
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 | |||
) | |||
</syntaxhighlight> | |||
== Автоматическое обновление программы из GitLab каждые 3 минуты == | |||
=== Назначение === | |||
Сервер периодически проверяет выбранную ветку GitLab и устанавливает новые коммиты. | |||
Если изменений нет, программа продолжает работать без перезапуска. | |||
При наличии обновления выполняются: | |||
# Проверка состояния репозитория. | |||
# Проверка допустимости изменений. | |||
# Сохранение предыдущего коммита. | |||
# Остановка приложения. | |||
# Обновление кода. | |||
# Проверка синтаксиса Python. | |||
# Запуск и проверка работоспособности. | |||
# Попытка отката при ошибке. | |||
'''Пример предназначен для Debian и Ubuntu с systemd.''' | |||
=== Область применения === | |||
Этот вариант обновляет код Python-приложения в существующем каталоге. Во время установки будет короткий простой. | |||
Он не устанавливает зависимости, не выполняет миграции базы, не пересобирает Docker-образы и не меняет системные настройки. | |||
Изменения за пределами разрешённых файлов требуют ручного обновления. | |||
'''Откат кода не отменяет изменения данных, которые успела сделать программа.''' Автоматизировать таким способом следует только совместимые обновления без изменения формата данных и обязательных миграций. | |||
=== Пример конфигурации === | |||
{| class="wikitable" | |||
! Параметр !! Значение | |||
|- | |||
| Каталог приложения || <code>/opt/my-app</code> | |||
|- | |||
| Владелец репозитория || <code>myapp</code> | |||
|- | |||
| Ветка для установки || <code>main</code> | |||
|- | |||
| Удалённый репозиторий || <code>origin</code> | |||
|- | |||
| Служба приложения || <code>my-app.service</code> | |||
|- | |||
| Виртуальное окружение || <code>/opt/my-app/.venv</code> | |||
|- | |||
| Исходники Python || <code>app/</code> и необязательный <code>main.py</code> | |||
|- | |||
| Проверка состояния || <code>http://127.0.0.1:8000/health</code> | |||
|- | |||
| Интервал || 3 минуты после завершения предыдущей проверки | |||
|} | |||
'''Все значения условные. Пользователь myapp, репозиторий, виртуальное окружение и служба должны уже существовать.''' | |||
Ветка для автоматической установки должна содержать только одобренные изменения. В GitLab следует ограничить прямую запись в неё и настроить прохождение CI перед слиянием. | |||
Сам скрипт не проверяет статус pipeline GitLab: публикация коммита в выбранной ветке считается разрешением на установку. | |||
=== 1. Подготовка === | |||
Команды настройки выполнять от имени root на сервере приложения. | |||
Установить необходимые инструменты: | |||
<syntaxhighlight lang="bash"> | |||
apt update | |||
apt install -y git curl util-linux | |||
</syntaxhighlight> | |||
Проверить службу: | |||
<syntaxhighlight lang="bash"> | |||
systemctl show my-app.service --no-pager \ | |||
-p User \ | |||
-p WorkingDirectory \ | |||
-p ExecStart | |||
systemctl status my-app.service --no-pager -l | |||
</syntaxhighlight> | |||
Она должна запускать программу из обновляемого каталога. Само приложение желательно запускать под отдельным непривилегированным пользователем. | |||
Проверить репозиторий от имени его владельца: | |||
<syntaxhighlight lang="bash"> | |||
runuser -u myapp -- git -C /opt/my-app status --short --branch | |||
runuser -u myapp -- git -C /opt/my-app branch --show-current | |||
</syntaxhighlight> | |||
Ожидается ветка main без локальных изменений и неотслеживаемых файлов. | |||
Логи, виртуальное окружение и локальные настройки должны находиться вне отслеживаемого кода либо быть корректно исключены через .gitignore. | |||
=== 2. Доступ сервера к GitLab === | |||
У пользователя myapp должен быть настроен доступ на чтение репозитория. | |||
Для SSH можно использовать отдельный Deploy Key с правами только на чтение. Ключ сервера GitLab должен быть заранее проверен и сохранён в known_hosts этого пользователя. | |||
Проверить доступ без интерактивного запроса пароля: | |||
<syntaxhighlight lang="bash"> | |||
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 | |||
</syntaxhighlight> | |||
Команда должна вывести идентификатор ветки и завершиться успешно. | |||
Не отключать проверку ключа SSH-сервера ради автоматизации. | |||
=== 3. Проверка состояния приложения === | |||
В примере приложение предоставляет адрес: | |||
<syntaxhighlight lang="text"> | |||
http://127.0.0.1:8000/health | |||
</syntaxhighlight> | |||
Проверить: | |||
<syntaxhighlight lang="bash"> | |||
curl --noproxy '*' --max-time 5 \ | |||
-sS -o /dev/null -w '%{http_code}\n' \ | |||
http://127.0.0.1:8000/health | |||
</syntaxhighlight> | |||
Ожидается HTTP 200. | |||
'''Этот endpoint должен существовать в приложении и отражать его готовность к работе.''' Не подставляйте произвольную страницу только потому, что она отвечает кодом 200. | |||
Для Telegram-бота без HTTP endpoint функцию проверки в скрипте нужно заменить реальной проверкой готовности бота. Одного состояния active недостаточно. | |||
=== 4. Создание скрипта обновления === | |||
Открыть: | |||
<syntaxhighlight lang="bash"> | |||
nano /usr/local/sbin/my-app-update | |||
</syntaxhighlight> | |||
Вставить: | |||
<syntaxhighlight lang="bash"> | |||
#!/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" | |||
</syntaxhighlight> | |||
Установить владельца, права и проверить синтаксис Bash: | |||
<syntaxhighlight lang="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 | |||
</syntaxhighlight> | |||
Отсутствие вывода при успешном завершении bash -n означает, что синтаксис Bash корректен. Это не проверка работоспособности всей процедуры. | |||
=== Как устроена защита === | |||
{| class="wikitable" | |||
! Механизм !! Назначение | |||
|- | |||
| <code>flock</code> | |||
| Не позволяет двум экземплярам скрипта одновременно менять приложение. | |||
|- | |||
| <code>git status</code> | |||
| Останавливает обновление при локальных изменениях. | |||
|- | |||
| <code>fetch</code> с ограничением времени | |||
| Проверяет GitLab без остановки программы. | |||
|- | |||
| <code>merge --ff-only</code> | |||
| Отказывается объединять разошедшиеся истории. | |||
|- | |||
| <code>--no-overwrite-ignore</code> | |||
| Не разрешает перезаписывать мешающие обновлению игнорируемые файлы. | |||
|- | |||
| Разрешённые пути | |||
| Отправляет изменения зависимостей и конфигурации на ручную установку. | |||
|- | |||
| Резервная ветка | |||
| Сохраняет ссылку на предыдущий коммит. | |||
|- | |||
| Проверка синтаксиса и HTTP | |||
| Проверяет код и ответ запущенного приложения. | |||
|- | |||
| Файл <code>PAUSED</code> | |||
| Блокирует дальнейшие установки после ошибки или прерванного обновления. | |||
|} | |||
Git, Python и HTTP-проверка выполняются от имени myapp. Root используется для управления службой и служебными файлами обновлятора. | |||
'''Список разрешённых путей не анализирует смысл кода.''' Даже изменение одного Python-файла может оказаться несовместимым с данными. Поэтому проверка изменений и CI перед публикацией в рабочую ветку обязательны. | |||
Во время работы обновлятора не выполнять ручные Git-операции в этом каталоге. Блокировка flock действует только на процессы, которые используют тот же файл блокировки. | |||
=== 5. Создание службы обновления === | |||
Открыть: | |||
<syntaxhighlight lang="bash"> | |||
nano /etc/systemd/system/my-app-update.service | |||
</syntaxhighlight> | |||
Вставить: | |||
<syntaxhighlight lang="ini"> | |||
[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 | |||
</syntaxhighlight> | |||
Это отдельная служба обновлятора. Существующую my-app.service заменять не нужно. | |||
TimeoutStartSec ограничивает длительность всей операции. При принудительном завершении откат не гарантирован: файл PAUSED сохраняется для ручного восстановления. | |||
=== 6. Создание таймера === | |||
Открыть: | |||
<syntaxhighlight lang="bash"> | |||
nano /etc/systemd/system/my-app-update.timer | |||
</syntaxhighlight> | |||
Вставить: | |||
<syntaxhighlight lang="ini"> | |||
[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 | |||
</syntaxhighlight> | |||
Параметры: | |||
* <code>OnBootSec=2min</code> — первая проверка через две минуты после загрузки. При включении таймера позднее этого момента она может начаться сразу. | |||
* <code>OnUnitInactiveSec=3min</code> — следующая проверка через три минуты после завершения предыдущего запуска. | |||
* <code>AccuracySec=10s</code> — допустимое окно срабатывания таймера. | |||
Для другого интервала заменить: | |||
<syntaxhighlight lang="ini"> | |||
OnUnitInactiveSec=2min | |||
</syntaxhighlight> | |||
или: | |||
<syntaxhighlight lang="ini"> | |||
OnUnitInactiveSec=5min | |||
</syntaxhighlight> | |||
Это интервал между проверками, а не гарантированный срок установки: сама операция тоже занимает время. | |||
=== 7. Первый запуск вручную === | |||
До включения расписания адаптировать пути, пользователя, ветку и healthcheck. | |||
Проверить файлы: | |||
<syntaxhighlight lang="bash"> | |||
systemd-analyze verify \ | |||
/etc/systemd/system/my-app-update.service \ | |||
/etc/systemd/system/my-app-update.timer | |||
systemctl daemon-reload | |||
</syntaxhighlight> | |||
Запустить одну проверку: | |||
<syntaxhighlight lang="bash"> | |||
systemctl start my-app-update.service | |||
journalctl -u my-app-update.service -n 100 --no-pager | |||
</syntaxhighlight> | |||
'''Этот запуск уже устанавливает новые коммиты, если они есть.''' Выполнять в согласованное время обслуживания. | |||
Сначала проверить весь сценарий на тестовой копии, включая успешное обновление и отказ проверки с откатом. | |||
После успешного выполнения oneshot-служба обычно имеет состояние inactive (dead). Для неё это нормально. | |||
Результат последнего запуска: | |||
<syntaxhighlight lang="bash"> | |||
systemctl show my-app-update.service \ | |||
-p Result \ | |||
-p ExecMainStatus | |||
</syntaxhighlight> | |||
Ожидается: | |||
<syntaxhighlight lang="text"> | |||
Result=success | |||
ExecMainStatus=0 | |||
</syntaxhighlight> | |||
=== 8. Включение расписания === | |||
После проверки включить таймер: | |||
<syntaxhighlight lang="bash"> | |||
systemctl enable --now my-app-update.timer | |||
systemctl list-timers --all my-app-update.timer | |||
</syntaxhighlight> | |||
В таблице будут показаны последнее и следующее срабатывания. | |||
=== Просмотр журналов === | |||
Журнал обновлятора: | |||
<syntaxhighlight lang="bash"> | |||
journalctl -u my-app-update.service -n 100 --no-pager | |||
</syntaxhighlight> | |||
Наблюдение в реальном времени: | |||
<syntaxhighlight lang="bash"> | |||
journalctl -u my-app-update.service -f | |||
</syntaxhighlight> | |||
Журнал самого приложения: | |||
<syntaxhighlight lang="bash"> | |||
journalctl -u my-app.service -n 100 --no-pager | |||
</syntaxhighlight> | |||
Пример штатной проверки: | |||
<syntaxhighlight lang="text"> | |||
Проверяем origin/main. | |||
Новых коммитов нет. | |||
</syntaxhighlight> | |||
=== Изменение интервала === | |||
Изменить OnUnitInactiveSec в файле таймера, затем выполнить: | |||
<syntaxhighlight lang="bash"> | |||
systemctl daemon-reload | |||
systemctl restart my-app-update.timer | |||
systemctl list-timers --all my-app-update.timer | |||
</syntaxhighlight> | |||
=== Временное отключение === | |||
Остановить расписание и отключить его автозапуск: | |||
<syntaxhighlight lang="bash"> | |||
systemctl disable --now my-app-update.timer | |||
</syntaxhighlight> | |||
Это не останавливает приложение и не прерывает уже выполняющееся обновление. | |||
Перед ручными работами проверить: | |||
<syntaxhighlight lang="bash"> | |||
systemctl status my-app-update.service --no-pager | |||
</syntaxhighlight> | |||
Дождаться завершения текущей операции. | |||
=== Что делать после ошибки обновления === | |||
Если ошибка произошла после начала установки, скрипт пытается вернуть предыдущий код. Дальнейшие установки блокируются файлом: | |||
<syntaxhighlight lang="text"> | |||
/var/lib/my-app-updater/PAUSED | |||
</syntaxhighlight> | |||
Отключить таймер и изучить состояние: | |||
<syntaxhighlight lang="bash"> | |||
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 | |||
</syntaxhighlight> | |||
После успешного отката репозиторий находится в detached HEAD на старом коммите. Ветка main может по-прежнему указывать на неудачное обновление. | |||
'''Не переключаться обратно на main и не удалять PAUSED вслепую.''' | |||
Для восстановления: | |||
# Исправить или отменить проблемное изменение в GitLab. | |||
# Вручную установить и проверить исправленную версию по процедуре ручного обновления. | |||
# Убедиться, что текущая ветка main, рабочая копия чистая и приложение исправно. | |||
# Только после этого снять паузу. | |||
<syntaxhighlight lang="bash"> | |||
rm /var/lib/my-app-updater/PAUSED | |||
systemctl reset-failed my-app-update.service | |||
systemctl enable --now my-app-update.timer | |||
</syntaxhighlight> | |||
При отключении питания, заполнении диска или принудительном завершении скрипта автоматический откат может не выполниться. Нужно проверить код и состояние службы через консоль. | |||
=== Изменение зависимостей или базы данных === | |||
Если в GitLab изменились requirements.txt, lock-файл, юнит, конфигурация или файлы миграций, обновлятор откажется устанавливать такой коммит. | |||
В этом случае: | |||
# Отключить таймер. | |||
# Дождаться завершения обновлятора. | |||
# Сделать необходимые резервные копии. | |||
# Выполнить ручное обновление по инструкции проекта. | |||
# Проверить приложение. | |||
# Снова включить таймер. | |||
Не добавлять автоматическую установку зависимостей или миграций без отдельного плана восстановления. | |||
=== Ограничения === | |||
* Резервная ветка сохраняет только код, а не базу и пользовательские файлы. | |||
* Успешный HTTP 200 не заменяет полноценные функциональные тесты. | |||
* Скрипт не поддерживает развёртывание Git LFS и submodules без дополнительной адаптации. | |||
* Приложение не должно самостоятельно менять отслеживаемые файлы. | |||
* На время обновления не должно быть внешнего механизма, который снова запускает остановленную службу. | |||
* Ошибки записываются в журнал; уведомления администратору здесь не настроены. | |||
* Резервные ветки постепенно накапливаются. Их удаление следует выполнять отдельно после проверки стабильности и наличия резервных копий. | |||
Текущая версия от 21:52, 28 сентября 2026
Небольшие инструкции, полезные команды и практические приёмы администрирования 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:
- Нажать
Control + O. - Нажать
Enter. - Нажать
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.
Затем:
- Восстановить совместимые зависимости и конфигурацию.
- При необходимости выполнить предусмотренное восстановление базы.
- Проверить приложение.
- Запустить службу и посмотреть журнал.
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 и устанавливает новые коммиты.
Если изменений нет, программа продолжает работать без перезапуска.
При наличии обновления выполняются:
- Проверка состояния репозитория.
- Проверка допустимости изменений.
- Сохранение предыдущего коммита.
- Остановка приложения.
- Обновление кода.
- Проверка синтаксиса Python.
- Запуск и проверка работоспособности.
- Попытка отката при ошибке.
Пример предназначен для 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 вслепую.
Для восстановления:
- Исправить или отменить проблемное изменение в GitLab.
- Вручную установить и проверить исправленную версию по процедуре ручного обновления.
- Убедиться, что текущая ветка main, рабочая копия чистая и приложение исправно.
- Только после этого снять паузу.
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-файл, юнит, конфигурация или файлы миграций, обновлятор откажется устанавливать такой коммит.
В этом случае:
- Отключить таймер.
- Дождаться завершения обновлятора.
- Сделать необходимые резервные копии.
- Выполнить ручное обновление по инструкции проекта.
- Проверить приложение.
- Снова включить таймер.
Не добавлять автоматическую установку зависимостей или миграций без отдельного плана восстановления.
Ограничения
- Резервная ветка сохраняет только код, а не базу и пользовательские файлы.
- Успешный HTTP 200 не заменяет полноценные функциональные тесты.
- Скрипт не поддерживает развёртывание Git LFS и submodules без дополнительной адаптации.
- Приложение не должно самостоятельно менять отслеживаемые файлы.
- На время обновления не должно быть внешнего механизма, который снова запускает остановленную службу.
- Ошибки записываются в журнал; уведомления администратору здесь не настроены.
- Резервные ветки постепенно накапливаются. Их удаление следует выполнять отдельно после проверки стабильности и наличия резервных копий.