feat(ops): обязательный backup БД и файлов перед деплоем
This commit is contained in:
@@ -12,3 +12,7 @@ LMS_WEB_PORT=8090
|
||||
# Домен (используется в deploy-скрипте/nginx).
|
||||
LMS_DOMAIN=lms.our-place.ru
|
||||
LETSENCRYPT_EMAIL=admin@our-place.ru
|
||||
|
||||
# Резервное копирование (см. deploy/BACKUP.md).
|
||||
LMS_BACKUP_DIR=/opt/lms/backups
|
||||
LMS_BACKUP_KEEP_DAYS=14
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ build/
|
||||
.env
|
||||
.env.prod
|
||||
/data/
|
||||
/backups/
|
||||
node_modules/
|
||||
# Локальные сессии Kilo Code — не часть репозитория.
|
||||
.kilo/
|
||||
|
||||
@@ -69,12 +69,15 @@ progress `:8083`, web `:8090`. Интерфейс: `http://localhost:8090`.
|
||||
|
||||
```bash
|
||||
cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD
|
||||
./deploy/deploy.sh up # сборка + запуск (postgres, 5 сервисов)
|
||||
./deploy/deploy.sh up # backup + сборка + запуск (postgres, 5 сервисов)
|
||||
./deploy/deploy.sh logs web
|
||||
./deploy/deploy.sh ps
|
||||
./deploy/deploy.sh down
|
||||
```
|
||||
|
||||
Перед обновлением `deploy.sh up` обязательно делает backup БД и файлов.
|
||||
Ежедневное расписание, retention и восстановление — в [`deploy/BACKUP.md`](deploy/BACKUP.md).
|
||||
|
||||
Наружу публикуется только web на `127.0.0.1:8090`; reverse proxy — nginx
|
||||
(конфиг и полная инструкция по деплою на `lms.our-place.ru` в
|
||||
[`deploy/DEPLOY.md`](deploy/DEPLOY.md) и `deploy/nginx/lms.our-place.ru.conf`).
|
||||
|
||||
114
deploy/BACKUP.md
Normal file
114
deploy/BACKUP.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Резервное копирование LMS
|
||||
|
||||
Резервное копирование обязательно:
|
||||
|
||||
- **перед каждым обновлением** (`./deploy/deploy.sh up` делает backup автоматически);
|
||||
- **ежедневно** по расписанию (systemd timer);
|
||||
- перед миграциями и любыми ручными изменениями схемы/данных.
|
||||
|
||||
## Что входит в backup
|
||||
|
||||
Один каталог `backups/lms-<UTC timestamp>/`:
|
||||
|
||||
| Файл | Содержимое |
|
||||
|---|---|
|
||||
| `db.dump` | `pg_dump --format=custom` всей БД `lms` |
|
||||
| `submissions.tar.gz` | volume `lms_lms_submissions` — решения студентов |
|
||||
| `assets.tar.gz` | volume `lms_lms_assets` — файлы материалов |
|
||||
| `SHA256SUMS` | контрольные суммы |
|
||||
| `manifest.txt` | дата, БД, пользователь, проект, retention |
|
||||
|
||||
Каталоги бэкапов создаются с правами `700`: внутри персональные данные.
|
||||
|
||||
## Настройки
|
||||
|
||||
В `.env.prod`:
|
||||
|
||||
```dotenv
|
||||
LMS_BACKUP_DIR=/opt/lms/backups
|
||||
LMS_BACKUP_KEEP_DAYS=14
|
||||
```
|
||||
|
||||
`LMS_BACKUP_KEEP_DAYS` — сколько дней хранить. `0` отключает автоудаление.
|
||||
|
||||
## Ручные команды
|
||||
|
||||
```bash
|
||||
./deploy/backup.sh backup # создать backup
|
||||
./deploy/backup.sh backup --if-running # пропустить, если стек не запущен
|
||||
./deploy/backup.sh list # список backup-каталогов
|
||||
./deploy/backup.sh verify <dir> # проверить суммы и целостность
|
||||
./deploy/backup.sh restore <dir> # восстановить (destructive)
|
||||
```
|
||||
|
||||
`restore` требует ввести `RESTORE`, останавливает прикладные сервисы,
|
||||
пересоздаёт БД, восстанавливает файлы и поднимает стек. PostgreSQL при этом
|
||||
не удаляется.
|
||||
|
||||
## Автоматически перед обновлением
|
||||
|
||||
`deploy/deploy.sh up` вызывает:
|
||||
|
||||
```bash
|
||||
./deploy/backup.sh backup --if-running
|
||||
```
|
||||
|
||||
Если стек ещё не запущен (первый деплой), шаг пропускается. Если backup
|
||||
завершился ошибкой — деплой прерывается.
|
||||
|
||||
## Ежедневное расписание (systemd)
|
||||
|
||||
```bash
|
||||
sudo cp deploy/systemd/lms-backup.service /etc/systemd/system/
|
||||
sudo cp deploy/systemd/lms-backup.timer /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now lms-backup.timer
|
||||
systemctl list-timers lms-backup.timer
|
||||
```
|
||||
|
||||
По умолчанию — каждый день в 03:00 с разбросом до 30 минут. `Persistent=true`
|
||||
означает, что пропущенный запуск выполнится после включения сервера.
|
||||
|
||||
Если systemd недоступен, cron:
|
||||
|
||||
```cron
|
||||
0 3 * * * cd /opt/lms && ./deploy/backup.sh backup >>/var/log/lms-backup.log 2>&1
|
||||
```
|
||||
|
||||
## Восстановление
|
||||
|
||||
```bash
|
||||
./deploy/backup.sh verify /opt/lms/backups/lms-20261023T030000Z
|
||||
./deploy/backup.sh restore /opt/lms/backups/lms-20261023T030000Z
|
||||
```
|
||||
|
||||
Проверка после восстановления:
|
||||
|
||||
```bash
|
||||
./deploy/deploy.sh ps
|
||||
curl -fsS http://127.0.0.1:8090/actuator/health
|
||||
```
|
||||
|
||||
## Внешняя копия
|
||||
|
||||
Backup на том же сервере не защищает от потери сервера. Настройте выгрузку:
|
||||
|
||||
```bash
|
||||
rsync -a --delete /opt/lms/backups/ backup-host:/srv/lms-backups/
|
||||
```
|
||||
|
||||
Требования:
|
||||
|
||||
- шифрование при передаче и хранении (backups содержат ПДн);
|
||||
- отдельный хост/хранилище;
|
||||
- проверка восстановления не реже раза в месяц на staging;
|
||||
- не хранить backup в git и в каталоге проекта без `.gitignore`.
|
||||
|
||||
## Проверка
|
||||
|
||||
Минимум раз в месяц:
|
||||
|
||||
1. `./deploy/backup.sh verify <dir>`.
|
||||
2. Развернуть копию на staging.
|
||||
3. Восстановить БД и файлы.
|
||||
4. Проверить вход, группу, лабу, сдачу и скачивание файла.
|
||||
@@ -96,24 +96,38 @@ sudo certbot --nginx -d lms.our-place.ru --redirect -m admin@our-place.ru --agre
|
||||
```bash
|
||||
cd /opt/lms
|
||||
git pull
|
||||
./deploy/deploy.sh up # пересоберёт изменённые образы и перезапустит
|
||||
./deploy/deploy.sh up # backup → сборка → миграции → перезапуск
|
||||
```
|
||||
|
||||
Миграции БД (Flyway) применяются автоматически при старте сервисов.
|
||||
`deploy.sh up` **обязательно** делает backup перед обновлением. Если backup
|
||||
завершился ошибкой, деплой прерывается. Миграции БД (Flyway) применяются
|
||||
автоматически при старте сервисов.
|
||||
|
||||
## 8. Резервное копирование
|
||||
|
||||
```bash
|
||||
# БД
|
||||
docker compose -f docker-compose.prod.yml --env-file .env.prod exec -T postgres \
|
||||
pg_dump -U "$LMS_DB_USER" "$LMS_DB_NAME" | gzip > lms-$(date +%F).sql.gz
|
||||
Полная инструкция — [`BACKUP.md`](BACKUP.md). Кратко:
|
||||
|
||||
# Файлы (решения и материалы)
|
||||
docker run --rm -v lms_lms_submissions:/sub -v lms_lms_assets:/assets -v "$PWD":/backup alpine \
|
||||
tar czf /backup/lms-files-$(date +%F).tar.gz -C / sub assets
|
||||
```bash
|
||||
./deploy/backup.sh backup # БД + решения + материалы
|
||||
./deploy/backup.sh list
|
||||
./deploy/backup.sh verify <dir>
|
||||
./deploy/backup.sh restore <dir> # destructive, требует ввода RESTORE
|
||||
```
|
||||
|
||||
Восстановление БД: `gunzip -c backup.sql.gz | docker compose ... exec -T postgres psql -U lms lms`.
|
||||
Backup включает `pg_dump` БД, volume решений и volume материалов, контрольные
|
||||
суммы и manifest. По умолчанию хранится 14 дней (`LMS_BACKUP_KEEP_DAYS`),
|
||||
каталог — `/opt/lms/backups` (`LMS_BACKUP_DIR`).
|
||||
|
||||
Ежедневное расписание:
|
||||
|
||||
```bash
|
||||
sudo cp deploy/systemd/lms-backup.service deploy/systemd/lms-backup.timer /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now lms-backup.timer
|
||||
```
|
||||
|
||||
Backup на том же сервере — не единственная защита: настройте выгрузку на
|
||||
отдельный хост и периодически проверяйте восстановление на staging.
|
||||
|
||||
## 9. Диагностика
|
||||
|
||||
|
||||
260
deploy/backup.sh
Executable file
260
deploy/backup.sh
Executable file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env bash
|
||||
# Резервное копирование и восстановление LMS.
|
||||
#
|
||||
# ./deploy/backup.sh backup # БД + файлы, с retention
|
||||
# ./deploy/backup.sh backup --if-running # пропустить, если стек не запущен
|
||||
# ./deploy/backup.sh list
|
||||
# ./deploy/backup.sh verify <dir>
|
||||
# ./deploy/backup.sh restore <dir> # destructive, требует ввода RESTORE
|
||||
#
|
||||
# Переменные:
|
||||
# LMS_BACKUP_DIR каталог бэкапов (по умолчанию ./backups)
|
||||
# LMS_BACKUP_KEEP_DAYS сколько дней хранить (по умолчанию 14)
|
||||
# LMS_COMPOSE_PROJECT имя compose-проекта (по умолчанию lms)
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
COMPOSE_FILE="docker-compose.prod.yml"
|
||||
ENV_FILE=".env.prod"
|
||||
BACKUP_ROOT="${LMS_BACKUP_DIR:-$PWD/backups}"
|
||||
KEEP_DAYS="${LMS_BACKUP_KEEP_DAYS:-14}"
|
||||
PROJECT="${LMS_COMPOSE_PROJECT:-lms}"
|
||||
|
||||
die() {
|
||||
echo "Ошибка: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
info() {
|
||||
echo ">> $*"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
usage:
|
||||
deploy/backup.sh backup [--if-running]
|
||||
deploy/backup.sh list
|
||||
deploy/backup.sh verify <backup-dir>
|
||||
deploy/backup.sh restore <backup-dir>
|
||||
|
||||
Переменные окружения: LMS_BACKUP_DIR, LMS_BACKUP_KEEP_DAYS, LMS_COMPOSE_PROJECT.
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_docker() {
|
||||
command -v docker >/dev/null 2>&1 || die "нужен Docker"
|
||||
docker compose version >/dev/null 2>&1 || die "нужен Docker Compose v2"
|
||||
}
|
||||
|
||||
load_env() {
|
||||
[[ -f "$ENV_FILE" ]] || die "не найден $ENV_FILE"
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
: "${LMS_DB_NAME:=lms}"
|
||||
: "${LMS_DB_USER:=lms}"
|
||||
# Переменные из .env.prod должны применяться и к путям backup.
|
||||
BACKUP_ROOT="${LMS_BACKUP_DIR:-$PWD/backups}"
|
||||
KEEP_DAYS="${LMS_BACKUP_KEEP_DAYS:-14}"
|
||||
PROJECT="${LMS_COMPOSE_PROJECT:-lms}"
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@"
|
||||
}
|
||||
|
||||
postgres_running() {
|
||||
compose ps --status running --services 2>/dev/null | grep -qx 'postgres'
|
||||
}
|
||||
|
||||
ensure_volumes() {
|
||||
local volume
|
||||
for volume in "${PROJECT}_lms_submissions" "${PROJECT}_lms_assets"; do
|
||||
docker volume inspect "$volume" >/dev/null 2>&1 || die "не найден volume $volume"
|
||||
done
|
||||
}
|
||||
|
||||
backup() {
|
||||
local if_running=0
|
||||
[[ "${1:-}" == "--if-running" ]] && if_running=1
|
||||
|
||||
require_docker
|
||||
load_env
|
||||
|
||||
if ! postgres_running; then
|
||||
if ((if_running)); then
|
||||
info "PostgreSQL не запущен — backup пропущен"
|
||||
return 0
|
||||
fi
|
||||
die "PostgreSQL не запущен; запустите стек или используйте --if-running"
|
||||
fi
|
||||
|
||||
ensure_volumes
|
||||
mkdir -p "$BACKUP_ROOT"
|
||||
chmod 700 "$BACKUP_ROOT"
|
||||
|
||||
# Не даём двум backup-процессам писать одновременно.
|
||||
exec 9>"$BACKUP_ROOT/.backup.lock"
|
||||
command -v flock >/dev/null 2>&1 || die "нужен flock (util-linux)"
|
||||
flock -n 9 || die "уже выполняется другой backup"
|
||||
|
||||
local timestamp dir tmp
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
dir="$BACKUP_ROOT/lms-$timestamp"
|
||||
tmp="$dir.tmp"
|
||||
|
||||
[[ ! -e "$dir" ]] || die "каталог уже существует: $dir"
|
||||
rm -rf "$tmp"
|
||||
mkdir -p "$tmp"
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -rf '$tmp'" EXIT
|
||||
|
||||
info "Дамп базы данных..."
|
||||
compose exec -T postgres sh -c \
|
||||
'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" --format=custom --no-owner --no-privileges' \
|
||||
>"$tmp/db.dump"
|
||||
[[ -s "$tmp/db.dump" ]] || die "pg_dump вернул пустой файл"
|
||||
|
||||
info "Архив файлов (решения и материалы)..."
|
||||
docker run --rm \
|
||||
-v "${PROJECT}_lms_submissions:/submissions:ro" \
|
||||
-v "${PROJECT}_lms_assets:/assets:ro" \
|
||||
-v "$tmp":/backup \
|
||||
postgres:18 sh -c \
|
||||
'tar czf /backup/submissions.tar.gz -C /submissions . && tar czf /backup/assets.tar.gz -C /assets .'
|
||||
gzip -t "$tmp/submissions.tar.gz"
|
||||
gzip -t "$tmp/assets.tar.gz"
|
||||
|
||||
cat >"$tmp/manifest.txt" <<EOF
|
||||
backup_version=1
|
||||
created_at=$timestamp
|
||||
db_name=$LMS_DB_NAME
|
||||
db_user=$LMS_DB_USER
|
||||
compose_project=$PROJECT
|
||||
keep_days=$KEEP_DAYS
|
||||
EOF
|
||||
(
|
||||
cd "$tmp"
|
||||
sha256sum db.dump submissions.tar.gz assets.tar.gz >SHA256SUMS
|
||||
)
|
||||
chmod 700 "$tmp"
|
||||
mv "$tmp" "$dir"
|
||||
trap - EXIT
|
||||
|
||||
info "Backup готов: $dir"
|
||||
du -sh "$dir"/* | sed 's/^/ /'
|
||||
|
||||
if [[ "$KEEP_DAYS" =~ ^[0-9]+$ ]] && ((KEEP_DAYS > 0)); then
|
||||
find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -name 'lms-*' \
|
||||
-mtime +"$KEEP_DAYS" -print -exec rm -rf {} + 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
list_backups() {
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
load_env
|
||||
fi
|
||||
if [[ ! -d "$BACKUP_ROOT" ]]; then
|
||||
echo "Каталог бэкапов не найден: $BACKUP_ROOT"
|
||||
return 0
|
||||
fi
|
||||
find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -name 'lms-*' -printf '%f\n' | sort
|
||||
}
|
||||
|
||||
verify_backup() {
|
||||
local dir="${1:-}"
|
||||
[[ -n "$dir" ]] || usage
|
||||
[[ -d "$dir" ]] || die "нет каталога: $dir"
|
||||
dir="$(cd "$dir" && pwd)"
|
||||
[[ -f "$dir/SHA256SUMS" ]] || die "нет SHA256SUMS"
|
||||
[[ -s "$dir/db.dump" ]] || die "нет db.dump"
|
||||
[[ -s "$dir/submissions.tar.gz" ]] || die "нет submissions.tar.gz"
|
||||
[[ -s "$dir/assets.tar.gz" ]] || die "нет assets.tar.gz"
|
||||
|
||||
require_docker
|
||||
(
|
||||
cd "$dir"
|
||||
sha256sum -c SHA256SUMS
|
||||
)
|
||||
gzip -t "$dir/submissions.tar.gz"
|
||||
gzip -t "$dir/assets.tar.gz"
|
||||
docker run --rm -v "$dir":/backup:ro postgres:18 \
|
||||
pg_restore --list /backup/db.dump >/dev/null
|
||||
info "Backup корректен: $dir"
|
||||
}
|
||||
|
||||
restore_backup() {
|
||||
local dir="${1:-}"
|
||||
[[ -n "$dir" ]] || usage
|
||||
[[ -d "$dir" ]] || die "нет каталога: $dir"
|
||||
dir="$(cd "$dir" && pwd)"
|
||||
verify_backup "$dir"
|
||||
load_env
|
||||
postgres_running || die "PostgreSQL не запущен"
|
||||
ensure_volumes
|
||||
|
||||
echo
|
||||
echo "Будут восстановлены БД '$LMS_DB_NAME' и файлы из:"
|
||||
echo " $dir"
|
||||
echo "Текущие данные будут удалены."
|
||||
read -r -p "Введите RESTORE для продолжения: " answer
|
||||
[[ "$answer" == "RESTORE" ]] || die "восстановление отменено"
|
||||
|
||||
info "Останавливаю прикладные сервисы..."
|
||||
compose stop web gateway identity academic progress >/dev/null 2>&1 || true
|
||||
|
||||
info "Пересоздаю базу данных..."
|
||||
compose exec -T postgres sh -c \
|
||||
'dropdb -U "$POSTGRES_USER" --if-exists "$POSTGRES_DB" && createdb -U "$POSTGRES_USER" "$POSTGRES_DB"'
|
||||
|
||||
info "Восстанавливаю БД..."
|
||||
compose exec -T postgres sh -c \
|
||||
'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --no-owner --no-privileges' \
|
||||
<"$dir/db.dump"
|
||||
|
||||
info "Восстанавливаю файлы..."
|
||||
docker run --rm \
|
||||
-v "${PROJECT}_lms_submissions:/submissions" \
|
||||
-v "$dir":/backup:ro \
|
||||
postgres:18 sh -c \
|
||||
'find /submissions -mindepth 1 -maxdepth 1 -exec rm -rf {} +; tar xzf /backup/submissions.tar.gz -C /submissions'
|
||||
docker run --rm \
|
||||
-v "${PROJECT}_lms_assets:/assets" \
|
||||
-v "$dir":/backup:ro \
|
||||
postgres:18 sh -c \
|
||||
'find /assets -mindepth 1 -maxdepth 1 -exec rm -rf {} +; tar xzf /backup/assets.tar.gz -C /assets'
|
||||
|
||||
info "Запускаю сервисы..."
|
||||
compose up -d
|
||||
local attempt
|
||||
for attempt in $(seq 1 40); do
|
||||
if curl -fsS "http://127.0.0.1:${LMS_WEB_PORT:-8090}/actuator/health" >/dev/null 2>&1; then
|
||||
info "Восстановление завершено, web здоров"
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
die "сервисы не поднялись за 80 секунд; проверьте ./deploy/deploy.sh logs web"
|
||||
}
|
||||
|
||||
case "${1:-backup}" in
|
||||
backup)
|
||||
shift
|
||||
backup "${1:-}"
|
||||
;;
|
||||
list)
|
||||
list_backups
|
||||
;;
|
||||
verify)
|
||||
verify_backup "${2:-}"
|
||||
;;
|
||||
restore)
|
||||
restore_backup "${2:-}"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
@@ -30,6 +30,8 @@ cmd="${1:-up}"
|
||||
|
||||
case "$cmd" in
|
||||
up)
|
||||
echo ">> Резервная копия перед обновлением (обязательно)..."
|
||||
./deploy/backup.sh backup --if-running
|
||||
echo ">> Сборка образов (Gradle внутри Docker, сервер Java не требует)..."
|
||||
$COMPOSE build
|
||||
echo ">> Запуск сервисов..."
|
||||
@@ -52,8 +54,20 @@ case "$cmd" in
|
||||
restart)
|
||||
$COMPOSE restart "${2:-}"
|
||||
;;
|
||||
backup)
|
||||
./deploy/backup.sh backup
|
||||
;;
|
||||
backups)
|
||||
./deploy/backup.sh list
|
||||
;;
|
||||
verify)
|
||||
./deploy/backup.sh verify "${2:-}"
|
||||
;;
|
||||
restore)
|
||||
./deploy/backup.sh restore "${2:-}"
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 [up|ps|logs <service>|restart [service]|down]" >&2
|
||||
echo "usage: $0 [up|ps|logs <service>|restart [service]|down|backup|backups|verify <dir>|restore <dir>]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
15
deploy/systemd/lms-backup.service
Normal file
15
deploy/systemd/lms-backup.service
Normal file
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=LMS backup (PostgreSQL + files)
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=/opt/lms
|
||||
ExecStart=/opt/lms/deploy/backup.sh backup
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
# Если сервис запускается не от root, раскомментируйте и укажите пользователя
|
||||
# с доступом к Docker и каталогу /opt/lms:
|
||||
# User=lms
|
||||
10
deploy/systemd/lms-backup.timer
Normal file
10
deploy/systemd/lms-backup.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Daily LMS backup
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:00:00
|
||||
Persistent=true
|
||||
RandomizedDelaySec=1800
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user