Пишем Bash скрипт для резервного копирования с помощью tar
Приветствую!

В этой заметке мы с вами напишем универсальный бэкап-скрипт на Bash, который с помощью утилиты tar будет создавать архивы с файлами локального или удалённых хостов, выполнять ротацию старых бэкапов, шифровать их на лету, а также контролировать дисковое пространство 😌.

Вводные данные

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

Примеры из статьи выполнялись в среде дистрибутива Linux Mint 22 (Ubuntu 24.04), но скрипт полностью портативен.

Что под капотом скрипта

Логика скрипта сводится к тому, чтобы сформировать подходящую pipeline-команду: tar локально или tar по SSH на удалённой машине → утилита сжатия → утилита шифрования gpg → файл на диск.

Ключевые моменты:

Переходим непосредственно к скрипту.

Скрипт

Создаём файл в любом удобном месте, например, с помощью vim:

BASH
vim ~/.local/bin/tar.sh
Нажмите, чтобы развернуть и увидеть больше

И наполняем его следующим содержимым:

BASH
#!/usr/bin/env bash

set -Eeuo pipefail

# ==== EDIT THESE ====
SOURCE_DIR="/etc"
SOURCE_FILE=()            # specific file(s)/dir(s) inside SOURCE_DIR; empty = everything
                           # e.g. SOURCE_FILE=("passwd" "My Documents")
OUTPUT_DIR="/var/backups/etc"
EXTRA_PARAMS=()           # extra tar flags, e.g. EXTRA_PARAMS=("--exclude=*.tmp")
COMPRESS_FORMAT="zstd"    # gzip|pigz|bzip2|pbzip2|xz|zstd|"" (empty = no compression)
COMPRESS_LEVEL="15"
ENCRYPT_PASSWORD=""       # empty = no encryption
FILENAME=""               # empty = auto-name from OUTPUT_DIR
LABEL_KEEP="0"            # mutually exclusive with LABEL_WEEKLY/LABEL_MONTHLY
LABEL_WEEKLY="0"
LABEL_MONTHLY="0"
REMOVE_OLD=""             # days; empty = no retention
KEEP="0"                  # 1 = when removing old backups, keep ones with 'keep' in the name
KEEP_WEEKLY=""            # keep the N most recent 'weekly'-labeled backups
KEEP_MONTHLY=""           # keep the N most recent 'monthly'-labeled backups
HASH_FILE="1"             # 1 = also write a .sha256 next to the archive
# SSH: leave SSH_HOST empty for a local backup. When set, tar runs on the
# remote host and streams the archive back (pull, not push).
SSH_HOST=""
SSH_USER="$(id -un)"
SSH_PORT="22"
SSH_KEY="$HOME/.ssh/id_ed25519"
SSH_EXTRA_PARAMS=()       # e.g. SSH_EXTRA_PARAMS=("-o" "ProxyJump=bastion")
# =====================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd -P)"
SCRIPT_PID="$$"

if [[ -t 2 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; then
    LOG_GREEN=$'\033[32m'; LOG_RED=$'\033[31m'; LOG_RESET=$'\033[0m'
else
    LOG_GREEN=""; LOG_RED=""; LOG_RESET=""
fi

log() {
    local msg="$*" color=""
    [[ "$msg" == ERROR:* ]] && color="$LOG_RED"
    printf '%s[%s]%s %s%s%s\n' "$LOG_GREEN" "$(date '+%Y-%m-%d %H:%M:%S')" "$LOG_RESET" "$color" "$msg" "$LOG_RESET" >&2
}

# --- preflight (mirrors lib/checks.sh's check_utils/check_disk_space) ---
check_utils() {
    local util
    for util in "$@"; do
        command -v "$util" &> /dev/null || { log "ERROR: required utility not found: $util"; exit 1; }
    done
}

check_disk_space() {
    local dir="$1" pct
    pct=$(df "$dir" --output=pcent | tail -n 1 | tr -d ' %')
    if (( pct > 95 )); then
        log "ERROR: more than 95% of disk space used in $dir (${pct}%)"
        exit 1
    fi
    return 0
}

# --- SSH (mirrors lib/ssh.sh: accept-new against a local known_hosts, and
# ssh_remote_exec's per-token %q quoting -- not a string built for eval) ---
ssh_build_argv() {
    SSH_ARGV=("ssh" "-q" "-o" "StrictHostKeyChecking=accept-new" \
        "-o" "UserKnownHostsFile=${SCRIPT_DIR}/.known_hosts_standalone")
    SSH_ARGV+=("${SSH_EXTRA_PARAMS[@]}")
    SSH_ARGV+=("-l" "$SSH_USER" "-p" "$SSH_PORT" "-i" "$SSH_KEY" "$SSH_HOST")
}

ssh_remote_exec() {
    local -n remote_argv="$1"
    ssh_build_argv
    local quoted=() tok
    for tok in "${remote_argv[@]}"; do
        quoted+=("$(printf '%q' "$tok")")
    done
    "${SSH_ARGV[@]}" "${quoted[@]}"
}

# --- retention (mirrors lib/retention.sh's remove_old_backup) ---
remove_old_backups() {
    local output_dir="$1" remove_old="$2" keep="$3" keep_weekly="$4" keep_monthly="$5" kind="${6:-file}"
    local valid_extensions=(".tar" ".sql" ".gz" ".bz2" ".xz" ".zst" ".gpg")
    local now weekly_entries=() monthly_entries=()
    now=$(date +%s)

    local entry_path
    for entry_path in "$output_dir"/*; do
        local entry_name
        entry_name=$(basename "$entry_path")

        if [[ "$kind" == "dir" ]]; then
            [[ -d "$entry_path" && ! -L "$entry_path" ]] || continue
        else
            [[ -f "$entry_path" ]] || continue
            local matched=0 ext
            for ext in "${valid_extensions[@]}"; do
                [[ "$entry_name" == *"$ext" ]] && matched=1
            done
            (( matched )) || continue
        fi

        (( keep )) && [[ "$entry_name" == *keep* ]] && continue

        if (( keep_weekly > 0 )) && [[ "$entry_name" == *weekly* ]]; then
            weekly_entries+=("$entry_path")
            continue
        fi
        if (( keep_monthly > 0 )) && [[ "$entry_name" == *monthly* ]]; then
            monthly_entries+=("$entry_path")
            continue
        fi

        local entry_last_mod
        entry_last_mod=$(stat -c %Y "$entry_path")
        if (( now - entry_last_mod > remove_old * 86400 )); then
            log "Deleting old backup: $entry_path"
            rm -rf "$entry_path" "${entry_path}.sha256"
        fi
    done

    clean_old_generations() {
        local n="$1" label="$2"
        shift 2
        local entries=("$@")
        (( ${#entries[@]} <= n )) && return 0
        local sorted=() e
        mapfile -t sorted < <(
            for e in "${entries[@]}"; do
                printf '%s\t%s\n' "$(stat -c %Y "$e")" "$e"
            done | sort -nr -k1,1 | cut -f2-
        )
        local old
        for old in "${sorted[@]:$n}"; do
            log "Deleting old $label backup: $old"
            rm -rf "$old" "${old}.sha256"
        done
        return 0
    }

    (( keep_weekly > 0 ))  && clean_old_generations "$keep_weekly" "weekly" "${weekly_entries[@]}"
    (( keep_monthly > 0 )) && clean_old_generations "$keep_monthly" "monthly" "${monthly_entries[@]}"
    return 0
}

[[ -n "$SSH_HOST" || -d "$SOURCE_DIR" ]] || { log "ERROR: source dir not found: $SOURCE_DIR"; exit 1; }

required_utils=("tar" "flock")
[[ -n "$COMPRESS_FORMAT" ]] && required_utils+=("$COMPRESS_FORMAT")
[[ -n "$ENCRYPT_PASSWORD" ]] && required_utils+=("gpg")
[[ -n "$SSH_HOST" ]] && required_utils+=("ssh")
check_utils "${required_utils[@]}"

labels=0
[[ "$LABEL_KEEP" == "1" ]]    && labels=$((labels + 1))
[[ "$LABEL_WEEKLY" == "1" ]]  && labels=$((labels + 1))
[[ "$LABEL_MONTHLY" == "1" ]] && labels=$((labels + 1))
(( labels > 1 )) && { log "ERROR: only one label allowed"; exit 1; }

mkdir -p "$OUTPUT_DIR"

[[ -z "$FILENAME" ]] && FILENAME="backup_tar_$(basename "$(realpath -m "$OUTPUT_DIR")")"

LOCK_FILE="${SCRIPT_DIR}/.${FILENAME}.lock"
exec {lock_fd}>> "$LOCK_FILE"
flock -n "$lock_fd" || { log "ERROR: another backup for this item is already running"; exit 1; }
echo "$SCRIPT_PID" > "$LOCK_FILE"

release_lock() {
    if [[ -f "$LOCK_FILE" ]] && [[ "$(< "$LOCK_FILE")" == "$SCRIPT_PID" ]]; then
        rm -f "$LOCK_FILE"
    fi
}
trap release_lock EXIT

[[ "$LABEL_KEEP" == "1" ]]    && FILENAME="${FILENAME}_keep"
[[ "$LABEL_WEEKLY" == "1" ]]  && FILENAME="${FILENAME}_weekly"
[[ "$LABEL_MONTHLY" == "1" ]] && FILENAME="${FILENAME}_monthly"
FILENAME="${FILENAME}_$(date '+%F_%H-%M-%S').tar"

if [[ -n "$REMOVE_OLD" ]]; then
    remove_old_backups "$OUTPUT_DIR" "$REMOVE_OLD" "$KEEP" "${KEEP_WEEKLY:-0}" "${KEEP_MONTHLY:-0}" file
fi

check_disk_space "$OUTPUT_DIR"

case "$COMPRESS_FORMAT" in
    gzip|pigz)    FILENAME="${FILENAME}.gz"  ;;
    bzip2|pbzip2) FILENAME="${FILENAME}.bz2" ;;
    xz)           FILENAME="${FILENAME}.xz"  ;;
    zstd)         FILENAME="${FILENAME}.zst" ;;
    "")           ;;
    *) log "ERROR: unsupported COMPRESS_FORMAT: $COMPRESS_FORMAT"; exit 1 ;;
esac
[[ -n "$ENCRYPT_PASSWORD" ]] && FILENAME="${FILENAME}.gpg"

RESULT_FILE="${OUTPUT_DIR}/${FILENAME}"

producer=("tar" "--create")
producer+=("${EXTRA_PARAMS[@]}")
producer+=("--file=-" "--directory=${SOURCE_DIR}")
if [[ ${#SOURCE_FILE[@]} -eq 0 ]]; then
    producer+=("./")
else
    producer+=("${SOURCE_FILE[@]}")
fi

run_producer() {
    if [[ -n "$SSH_HOST" ]]; then
        ssh_remote_exec producer
    else
        "${producer[@]}"
    fi
}

run_compress() {
    case "$COMPRESS_FORMAT" in
        gzip|pigz)    "$COMPRESS_FORMAT" "-$COMPRESS_LEVEL" ;;
        bzip2|pbzip2) "$COMPRESS_FORMAT" "-$COMPRESS_LEVEL" ;;
        xz)           xz "-$COMPRESS_LEVEL" --stdout ;;
        zstd)         zstd "-$COMPRESS_LEVEL" -c ;;
        *)            cat ;;
    esac
}

run_encrypt() {
    if [[ -z "$ENCRYPT_PASSWORD" ]]; then
        cat
        return
    fi
    local tmpdir passfile
    tmpdir="$(mktemp -d /dev/shm/.encrypt.XXXXXX)"
    passfile="$tmpdir/passfile"
    trap 'rm -rf "$tmpdir"' RETURN
    printf "%s" "$ENCRYPT_PASSWORD" > "$passfile"
    chmod 600 "$passfile"
    gpg --quiet --batch --yes --symmetric --cipher-algo AES256 --homedir "$tmpdir" --passphrase-file "$passfile"
}

log "Running: ${producer[*]}$([[ -n "$SSH_HOST" ]] && echo " (on $SSH_HOST)") > $RESULT_FILE"

if run_producer | run_compress | run_encrypt > "$RESULT_FILE"; then
    log "Backup created: $RESULT_FILE"
else
    rm -f "$RESULT_FILE"
    log "ERROR: backup failed"
    exit 1
fi

if [[ "$HASH_FILE" == "1" ]]; then
    sha256sum "$RESULT_FILE" > "${RESULT_FILE}.sha256"
    log "Hash: ${RESULT_FILE}.sha256"
fi
Нажмите, чтобы развернуть и увидеть больше

Изменять нужно будет только переменные между строками # ==== EDIT THESE ==== и # =====================. И то не все.

Тут не стану расписывать все блоки скрипта, при необходимости запросите пояснения у нейронок. У них это сегодня неплохо получается 😉.

Примеры использования

Ниже я покажу несколько сценариев использования нашего скрипта. Все примеры рабочие и безопасные - используют тестовые директории в /tmp/, а не реальные данные.

Подготовка тестового окружения

Сначала создадим тестовое окружение с данными:

BASH
# Создаём директории
mkdir -vp /tmp/backup-demo/{data,config}

# Генерируем тестовые файлы разного размера
dd if=/dev/zero of=/tmp/backup-demo/data/file1.bin bs=1M count=10
dd if=/dev/zero of=/tmp/backup-demo/data/file2.bin bs=1M count=5
echo "app_version=1.0" > /tmp/backup-demo/config/app.conf
echo "db_host=localhost" > /tmp/backup-demo/config/db.conf
echo "secret_token=abc123" > /tmp/backup-demo/config/.env

# Выходные директории для бэкапов
mkdir -vp /tmp/backups/{local,remote,weekly,monthly,encrypted}

# Проверяем, что все готово
ls -lh /tmp/backup-demo/
du -sh /tmp/backup-demo/*
Нажмите, чтобы развернуть и увидеть больше

Пример 1: Простое резервное копирование с сжатием zstd

Простой кейс - копируем директорию с данными, сжимаем архив современным алгоритмом zstd (хороший баланс скорости и степени сжатия). Получаем архив с хешем для проверки целостности. Это подходит для ежедневных бэкапов без особых требований.

В нашем скрипте корректируем следующие переменные:

BASH
#!/usr/bin/env bash

SOURCE_DIR="/tmp/backup-demo"
OUTPUT_DIR="/tmp/backups/local"
COMPRESS_FORMAT="zstd"
COMPRESS_LEVEL="15"
HASH_FILE="1"
SSH_HOST=""
Нажмите, чтобы развернуть и увидеть больше

Добавляем флаг исполнения скрипту и запускаем его:

BASH
chmod +x ~/.local/bin/tar.sh

~/.local/bin/tar.sh
Нажмите, чтобы развернуть и увидеть больше

После выполнения увидим подобный вывод в терминале:

Проверим целостность архива:

BASH
bash -c 'cd /tmp/backups/local/; sha256sum -c *.sha256'
Нажмите, чтобы развернуть и увидеть больше

Хеши должны совпадать:

Просмотреть содержимое архива можно с помощью того же tar и ключа -t:

BASH
tar -tvf /tmp/backups/local/backup_tar_local_2026-08-20_07-38-39.tar.zst
Нажмите, чтобы развернуть и увидеть больше

Пример 2: Резервное копирование конкретных файлов с шифрованием GPG

Не всегда нужна вся директория целиком. Для этого наш скрипт умеет копировать отдельные файлы, например, конфиги + добавляем шифрование AES256 с помощью утилиты gpg.

Правим переменные:

BASH
#!/usr/bin/env bash

SOURCE_DIR="/tmp/backup-demo"
SOURCE_FILE=("config")           # только папка config, без data/
OUTPUT_DIR="/tmp/backups/encrypted"
COMPRESS_FORMAT="zstd"
COMPRESS_LEVEL="15"
ENCRYPT_PASSWORD="i-like-r4ven-me"  # в боевом коде лучше читать из переменной окружения
HASH_FILE="1"
SSH_HOST=""
Нажмите, чтобы развернуть и увидеть больше

Стартуем:

BASH
~/.local/bin/tar.sh
Нажмите, чтобы развернуть и увидеть больше

Создадим директорию /tmp/restore и попробуем распаковать и расшифровать (будет запрошен пароль) наш архив в неё:

BASH
mkdir -vp /tmp/restore/

gpg --quiet --decrypt /tmp/backups/encrypted/*.tar.zst.gpg | zstd -d | tar -xvC /tmp/restore/
Нажмите, чтобы развернуть и увидеть больше

Пример 3: Резервное копирование с ротацией и retention политикой

Бэкапы имеют свойство накапливаться, поэтому их нужно грамотно ротировать. Настроим следующую политику хранения: архивы старше 7 дней - удаляем, если имя содержит “keep” - сохраняем всегда, остальное по сроку. Запустим скрипт несколько раз, чтобы увидеть удаление.

Корректируем переменные скрипта:

BASH
#!/usr/bin/env bash

SOURCE_DIR="/tmp/backup-demo"
OUTPUT_DIR="/tmp/backups/weekly"
COMPRESS_FORMAT="zstd"
COMPRESS_LEVEL="12"
HASH_FILE="1"
REMOVE_OLD="7"              # удаляем бэкапы старше 7 дней
KEEP="1"                    # если в имени файла есть "keep" - не удаляем
KEEP_WEEKLY="3"             # оставляем 3 последних weekly бэкапа
LABEL_WEEKLY="1"            # добавляем suffix "_weekly"
SSH_HOST=""
Нажмите, чтобы развернуть и увидеть больше

Запускаем несколько раз, чтобы создать N-е количество архивов:

BASH
for i in {1..5}; do ~/.local/bin/tar.sh; sleep 1; echo '---'; done
Нажмите, чтобы развернуть и увидеть больше

Видим, что при создании 5го архива, скрипт видит 4 weekly архива (а должно оставаться 3 - KEEP_WEEKLY="3") и удаляем самый старый из них. В итоге получится 4 бэкапа: 3 сохраняем + последний.

Теперь давайте отключим сохранение недельных копий (KEEP_WEEKLY="0") и дату последнего изменения одного из архивов искусственно уменьшим на 8 дней:

BASH
sed -i 's/KEEP_WEEKLY="3"/KEEP_WEEKLY="0"/' ~/.local/bin/tar.sh

touch -d '-8 days' /tmp/backups/weekly/backup_tar_weekly_2026-08-20_08-22-37.tar.zst.gpg
Нажмите, чтобы развернуть и увидеть больше

Запустим скрипт еще раз:

BASH
~/.local/bin/tar.sh
Нажмите, чтобы развернуть и увидеть больше

Видим, что копия старше 7 дней была удалена.

Теперь давайте еще один файл сделаем старше 7 дней, но добавим к существующему файлу в имя слово keep:

BASH
touch -d '-8 days' /tmp/backups/weekly/backup_tar_weekly_2026-08-20_08-45-21.tar.zst.gpg

mv -v /tmp/backups/weekly/backup_tar_weekly_{,keep_}2026-08-20_08-45-21.tar.zst.gpg
Нажмите, чтобы развернуть и увидеть больше

Пробуем выполнить бэкап:

BASH
~/.local/bin/tar.sh
Нажмите, чтобы развернуть и увидеть больше

Ничего не было удалено.

Но если вновь вернуть старое имя без keep и запустить ещё раз:

BASH
mv -v /tmp/backups/weekly/backup_tar_weekly_{keep_,}2026-08-20_08-45-21.tar.zst.gpg

~/.local/bin/tar.sh
Нажмите, чтобы развернуть и увидеть больше

Увидим ожидаемый результат:

Пример 4: Резервное копирование по SSH из docker контейнера

Бывает нужно скопировать данные с удаленного хоста по SSH. Чтобы не засорять и не нагружать лишний раз наши машины, используем Docker контейнер с демоном sshd внутри. Скрипт подключится к контейнеру и скопирует его файлы.

Сначала подготовим Docker окружение с помощью cat и here doc:

BASH
# Создадим простой Dockerfile с sshd
mkdir -vp /tmp/ssh-demo

cat > /tmp/ssh-demo/Dockerfile << 'EOF'
FROM ubuntu:22.04

RUN apt-get update && apt-get install -y openssh-server tar zstd && \
    mkdir -p /run/sshd && \
    mkdir -p /root/.ssh && \
    ssh-keygen -A

# Разрешим root логин (только для демо!)
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \
    mkdir -p /root/.ssh/authorized_keys && \
    chmod 700 /root/.ssh/authorized_keys

CMD ["/usr/sbin/sshd", "-D"]
EOF

# Собираем образ
docker build -t backup-demo-sshd /tmp/ssh-demo/

# Запускаем контейнер
docker run -d --name backup-demo-ssh -p 2222:22 -v /tmp/backup-demo:/data:ro backup-demo-sshd
Нажмите, чтобы развернуть и увидеть больше

Импортируем наш публичный ключ для пользователя root в контейнере:

BASH
docker exec backup-demo-ssh sh -c "echo $(cat ~/.ssh/id_ed25519.pub) > /root/.ssh/authorized_keys"

docker exec backup-demo-ssh chmod 600 /root/.ssh/authorized_keys
Нажмите, чтобы развернуть и увидеть больше

Теперь правим наш скрипт резервного копирования:

BASH
#!/usr/bin/env bash

SOURCE_DIR="/data"               # путь на удаленной машине
SOURCE_FILE=()
OUTPUT_DIR="/tmp/backups/remote"
COMPRESS_FORMAT="zstd"
COMPRESS_LEVEL="15"
ENCRYPT_PASSWORD=""
HASH_FILE="1"
SSH_HOST="127.0.0.1"             # или IP контейнера
SSH_USER="root"
SSH_PORT="2222"                  # порт который мы открыли
SSH_KEY="$HOME/.ssh/id_ed25519"  # или любой другой ваш ключ
SSH_EXTRA_PARAMS=("-C")
Нажмите, чтобы развернуть и увидеть больше

Запускаем бэкап:

BASH
~/.local/bin/tar.sh
Нажмите, чтобы развернуть и увидеть больше

Проверим результат:

BASH
ls -lh /tmp/backups/remote/

# Распакуем и проверим
mkdir -vp /tmp/restore-ssh

tar -xf /tmp/backups/remote/backup_tar_remote_2026-08-20_09-34-15.tar.zst -C /tmp/restore-ssh/
Нажмите, чтобы развернуть и увидеть больше

Не забываем почистить Docker:

BASH
docker stop backup-demo-ssh

docker rm backup-demo-ssh

docker rmi backup-demo-sshd
Нажмите, чтобы развернуть и увидеть больше

Пример 5: Ежемесячные и еженедельные метки с разной ротацией

В боевых системах часто нужна стратегия “держи все дневные бэкапы неделю, все недельные 3 месяца, все месячные год”. Настроим скрипт с метками и разными retention политиками.

BASH
#!/usr/bin/env bash

SOURCE_DIR="/tmp/backup-demo"
OUTPUT_DIR="/tmp/backups/monthly"
COMPRESS_FORMAT="zstd"
COMPRESS_LEVEL="12"
HASH_FILE="1"
REMOVE_OLD="1"              # удаляем старше 1 дня (для демо)
KEEP_MONTHLY="3"            # держим 3 последних месячных бэкапа
LABEL_MONTHLY="1"           # добавляем suffix "_monthly"
SSH_HOST=""
Нажмите, чтобы развернуть и увидеть больше

Запускаем бэкап:

BASH
~/.local/bin/tar.sh
Нажмите, чтобы развернуть и увидеть больше

Теперь симулируем несколько месячных бэкапов:

BASH
for i in {1..5}; do ~/.local/bin/tar.sh; echo '---'; sleep 1; done

ls -l /tmp/backups/monthly/*.tar.zst
Нажмите, чтобы развернуть и увидеть больше

В директории останется только 3 месячных бэкапа, старые будут удалены.

Пример 6: Резервное копирование с исключением файлов

При копировании больших директорий, часто нужно исключить временные файлы, логи, кэши и пр. Добавим флаги tar для фильтрации:

BASH
#!/usr/bin/env bash

SOURCE_DIR="/tmp/backup-demo"
OUTPUT_DIR="/tmp/backups/local"
COMPRESS_FORMAT="zstd"
COMPRESS_LEVEL="15"
HASH_FILE="1"
EXTRA_PARAMS=(
    "--exclude=*.tmp"
    "--exclude=.cache"
    "--exclude=.env"
    "--exclude=__pycache__"
)
SSH_HOST=""
Нажмите, чтобы развернуть и увидеть больше

Бэкапимся:

BASH
~/.local/bin/tar.sh
Нажмите, чтобы развернуть и увидеть больше

Проверим что существующий .env файл не попал в архив:

BASH
tar -tvf /tmp/backups/local/backup_tar_local_2026-08-20_17-54-30.tar.zst
Нажмите, чтобы развернуть и увидеть больше

Пример 7: Резервное копирование без сжатия для максимальной скорости

Если нужна максимальная скорость, то можно отключить сжатие, получаем чистый TAR архив:

BASH
#!/usr/bin/env bash

SOURCE_DIR="/tmp/backup-demo"
OUTPUT_DIR="/tmp/backups/local"
COMPRESS_FORMAT=""          # пусто - без сжатия
COMPRESS_LEVEL=""
HASH_FILE="1"
EXTRA_PARAMS=("--verbose")  # видим процесс
SSH_HOST=""
Нажмите, чтобы развернуть и увидеть больше

Запускаем скрипт:

BASH
~/.local/bin/tar.sh
Нажмите, чтобы развернуть и увидеть больше

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

Очистка тестового окружения

После экспериментов удалим тестовые данные:

BASH
rm -vrf /tmp/backup-demo /tmp/backups /tmp/ssh-demo /tmp/restore*
Нажмите, чтобы развернуть и увидеть больше

Послесловие

Скрипт получился довольно универсальным. Покрывает большинство задач для несложного бэкапа файлов/папок.

Он также имеет некоторые механизмы защиты, например, проверку доступного дискового пространства (должно быть занято не более 95%), в противном случае ошибка:

TXT
~/.local/bin/tar.sh
[2026-08-20 18:50:54] ERROR: more than 95% of disk space used in /tmp/backups/local (97%)
Нажмите, чтобы развернуть и увидеть больше

Или предотвращение race condition: если один экземпляр скрипта еще не завершился, запуск второго с такими же параметрами не произойдёт:

TXT
~/.local/bin/tar.sh
[2026-08-20 18:53:18] ERROR: another backup for this item is already running
Нажмите, чтобы развернуть и увидеть больше

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

Ну а если вы ищете комплексное решение для вашей Self hosted инфраструктуры, то рекомендую рассмотреть такой великолепный инструмент, как Arkeep - сервер с GUI панелью, в основе работы которого лежат замечательные restic и rclone.

Спасибо, что читаете! Старайтесь уделять больше времени, своим бэкапам.

Полезные материалы

Комментарии

Авторские права

Автор: Иван Чёрный

Ссылка: https://r4ven.me/automation/pishem-bash-skript-dlya-rezervnogo-kopirovaniya-s-pomoshchyu-tar/

Лицензия: CC BY-NC-SA 4.0

Использование материалов блога разрешается при условии: указания авторства/источника, некоммерческого использования и сохранения лицензии.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша